From a0fa8a489bb4af98059e5a064c7fac2a77b49aff Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 22 Apr 2024 10:44:05 +0200 Subject: [PATCH 001/101] ruby: Check if `solargraph` exists in `$PATH` or is configured (#10835) This fixes #9811 by checking for the `solargraph` binary in the `$PATH` as it's setup in the project shell. It also adds support for configuring the path to `solargraph` manually: ```json { "lsp": { "solargraph": { "binary": { "path": "/Users/thorstenball/bin/solargraph", "arguments": ["stdio"] } } } } ``` ## Example Given the following setup: - `ruby@3.3.0` used globally, no `solargraph` installed globally - `ruby@3.2.2` used in a project, `solargraph` installed as binstub in `$project/bin/solargraph`, `.envrc` to configure `direnv` to add `$project/bin` to `$PATH Which looks like this in practice: ```shell # GLOBAL ~ $ ruby --version ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23] ~ $ which solargraph solargraph not found # IN PROJECT ~ $ cd work/projs/rails-proj direnv: loading ~/work/projs/rails-proj/.envrc direnv: export ~PATH ~/work/projs/rails-proj $ ruby --version ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin23] ~/work/projs/rails-proj $ which solargraph /Users/thorstenball/work/projs/rails-proj/bin/solargraph ``` The expectation is that Zed, when opening `~/work/projs/rails-proj`, picks up the local `solargraph`. But with **Zed Stable** that doesn't work, as we can see in the logs: ``` 2024-04-22T10:21:37+02:00 [INFO] starting language server. binary path: "solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"] 2024-04-22T10:21:37+02:00 [ERROR] failed to start language server "solargraph": No such file or directory (os error 2) ``` With the change in this PR, it uses `rails/proj/bin/solargraph`: ``` [2024-04-22T10:33:06+02:00 INFO language] found user-installed language server for Ruby. path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", arguments: ["stdio"] [2024-04-22T10:33:06+02:00 INFO lsp] starting language server. binary path: "/Users/thorstenball/work/projs/rails-proj/bin/solargraph", working directory: "/Users/thorstenball/work/projs/rails-proj", args: ["stdio"] ``` **NOTE**: depending on whether `mise` (or `rbenv`, `asdf`, `chruby`, ...) or `direnv` come first in the shell-rc file, it picks one or the other, depending on what puts itself first in `$PATH`. ## Release Notes Release Notes: - Added support for finding the Ruby language server `solargraph` in the user's `$PATH` as it is when `cd`ing into a project's directory. ([#9811](https://github.com/zed-industries/zed/issues/9811)) - Added support for configuring the `path` and `arguments` for `solargraph` language server manually. Example from settings: `{"lsp": {"solargraph": {"binary": {"path":"/Users/thorstenball/bin/solargraph","arguments": ["stdio"]}}}}` ([#9811](https://github.com/zed-industries/zed/issues/9811)) --- crates/languages/src/ruby.rs | 54 ++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/ruby.rs b/crates/languages/src/ruby.rs index 8a634b6bc4..f7c32f8f85 100644 --- a/crates/languages/src/ruby.rs +++ b/crates/languages/src/ruby.rs @@ -1,15 +1,63 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; +use gpui::AsyncAppContext; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; -use std::{any::Any, path::PathBuf, sync::Arc}; +use project::project_settings::{BinarySettings, ProjectSettings}; +use settings::Settings; +use std::{any::Any, ffi::OsString, path::PathBuf, sync::Arc}; pub struct RubyLanguageServer; +impl RubyLanguageServer { + const SERVER_NAME: &'static str = "solargraph"; + + fn server_binary_arguments() -> Vec { + vec!["stdio".into()] + } +} + #[async_trait(?Send)] impl LspAdapter for RubyLanguageServer { fn name(&self) -> LanguageServerName { - LanguageServerName("solargraph".into()) + LanguageServerName(Self::SERVER_NAME.into()) + } + + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + cx: &AsyncAppContext, + ) -> Option { + let configured_binary = cx.update(|cx| { + ProjectSettings::get_global(cx) + .lsp + .get(Self::SERVER_NAME) + .and_then(|s| s.binary.clone()) + }); + + if let Ok(Some(BinarySettings { + path: Some(path), + arguments, + })) = configured_binary + { + Some(LanguageServerBinary { + path: path.into(), + arguments: arguments + .unwrap_or_default() + .iter() + .map(|arg| arg.into()) + .collect(), + env: None, + }) + } else { + let env = delegate.shell_env().await; + let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; + Some(LanguageServerBinary { + path, + arguments: Self::server_binary_arguments(), + env: Some(env), + }) + } } async fn fetch_latest_server_version( @@ -36,7 +84,7 @@ impl LspAdapter for RubyLanguageServer { Some(LanguageServerBinary { path: "solargraph".into(), env: None, - arguments: vec!["stdio".into()], + arguments: Self::server_binary_arguments(), }) } From dd41c100995580393fde15e2e8b28b03a783af6d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 22 Apr 2024 12:36:26 +0300 Subject: [PATCH 002/101] Filter out other languages' tasks from the task modal (#10839) Release Notes: - Fixed tasks modal showing history from languages, not matching the currently active buffer's one --- crates/project/src/task_inventory.rs | 7 + crates/tasks_ui/src/lib.rs | 1 + crates/tasks_ui/src/modal.rs | 206 ++++++++++++++++++++++++++- 3 files changed, 209 insertions(+), 5 deletions(-) diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 58e2f9339a..302aa82d50 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -219,6 +219,13 @@ impl Inventory { .iter() .rev() .filter(|(_, task)| !task.original_task().ignore_previously_resolved) + .filter(|(task_kind, _)| { + if matches!(task_kind, TaskSourceKind::Language { .. }) { + Some(task_kind) == task_source_kind.as_ref() + } else { + true + } + }) .fold( HashMap::default(), |mut tasks, (task_source_kind, resolved_task)| { diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 6458aca7f5..2433ca93ce 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -470,6 +470,7 @@ mod tests { pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { let state = AppState::test(cx); + file_icons::init((), cx); language::init(cx); crate::init(cx); editor::init(cx); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index f3587f94c3..db91c817f6 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -418,21 +418,23 @@ impl PickerDelegate for TasksModalDelegate { #[cfg(test)] mod tests { - use std::path::PathBuf; + use std::{path::PathBuf, sync::Arc}; use editor::Editor; use gpui::{TestAppContext, VisualTestContext}; - use language::Point; + use language::{ContextProviderWithTasks, Language, LanguageConfig, LanguageMatcher, Point}; use project::{FakeFs, Project}; use serde_json::json; + use task::TaskTemplates; + use workspace::CloseInactiveTabsAndPanes; - use crate::modal::Spawn; + use crate::{modal::Spawn, tests::init_test}; use super::*; #[gpui::test] async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { - crate::tests::init_test(cx); + init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -580,7 +582,7 @@ mod tests { #[gpui::test] async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) { - crate::tests::init_test(cx); + init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( "/dir", @@ -672,6 +674,200 @@ mod tests { cx.executor().run_until_parked(); } + #[gpui::test] + async fn test_language_task_filtering(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + "a1.ts": "// a1", + "a2.ts": "// a2", + "b.rs": "// b", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + project.read_with(cx, |project, _| { + let language_registry = project.languages(); + language_registry.add(Arc::new( + Language::new( + LanguageConfig { + name: "TypeScript".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..LanguageMatcher::default() + }, + ..LanguageConfig::default() + }, + None, + ) + .with_context_provider(Some(Arc::new( + ContextProviderWithTasks::new(TaskTemplates(vec![ + TaskTemplate { + label: "Task without variables".to_string(), + command: "npm run clean".to_string(), + ..TaskTemplate::default() + }, + TaskTemplate { + label: "TypeScript task from file $ZED_FILE".to_string(), + command: "npm run build".to_string(), + ..TaskTemplate::default() + }, + TaskTemplate { + label: "Another task from file $ZED_FILE".to_string(), + command: "npm run lint".to_string(), + ..TaskTemplate::default() + }, + ])), + ))), + )); + language_registry.add(Arc::new( + Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..LanguageMatcher::default() + }, + ..LanguageConfig::default() + }, + None, + ) + .with_context_provider(Some(Arc::new( + ContextProviderWithTasks::new(TaskTemplates(vec![TaskTemplate { + label: "Rust task".to_string(), + command: "cargo check".into(), + ..TaskTemplate::default() + }])), + ))), + )); + }); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + let _ts_file_1 = workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/dir/a1.ts"), true, cx) + }) + .await + .unwrap(); + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + task_names(&tasks_picker, cx), + vec![ + "Another task from file /dir/a1.ts", + "TypeScript task from file /dir/a1.ts", + "Task without variables", + ], + "Should open spawn TypeScript tasks for the opened file, tasks with most template variables above, all groups sorted alphanumerically" + ); + emulate_task_schedule( + tasks_picker, + &project, + "TypeScript task from file /dir/a1.ts", + cx, + ); + + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["TypeScript task from file /dir/a1.ts", "Another task from file /dir/a1.ts", "Task without variables"], + "After spawning the task and getting it into the history, it should be up in the sort as recently used" + ); + tasks_picker.update(cx, |_, cx| { + cx.emit(DismissEvent); + }); + drop(tasks_picker); + cx.executor().run_until_parked(); + + let _ts_file_2 = workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, cx) + }) + .await + .unwrap(); + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + task_names(&tasks_picker, cx), + vec![ + "TypeScript task from file /dir/a1.ts", + "Another task from file /dir/a2.ts", + "TypeScript task from file /dir/a2.ts", + "Task without variables" + ], + "Even when both TS files are open, should only show the history (on the top), and tasks, resolved for the current file" + ); + tasks_picker.update(cx, |_, cx| { + cx.emit(DismissEvent); + }); + drop(tasks_picker); + cx.executor().run_until_parked(); + + let _rs_file = workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/dir/b.rs"), true, cx) + }) + .await + .unwrap(); + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + task_names(&tasks_picker, cx), + vec!["Rust task"], + "Even when both TS files are open and one TS task spawned, opened file's language tasks should be displayed only" + ); + + cx.dispatch_action(CloseInactiveTabsAndPanes::default()); + emulate_task_schedule(tasks_picker, &project, "Rust task", cx); + let _ts_file_2 = workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/dir/a2.ts"), true, cx) + }) + .await + .unwrap(); + let tasks_picker = open_spawn_tasks(&workspace, cx); + assert_eq!( + task_names(&tasks_picker, cx), + vec![ + "TypeScript task from file /dir/a1.ts", + "Another task from file /dir/a2.ts", + "TypeScript task from file /dir/a2.ts", + "Task without variables" + ], + "After closing all but *.rs tabs, running a Rust task and switching back to TS tasks, \ + same TS spawn history should be restored" + ); + } + + fn emulate_task_schedule( + tasks_picker: View>, + project: &Model, + scheduled_task_label: &str, + cx: &mut VisualTestContext, + ) { + let scheduled_task = tasks_picker.update(cx, |tasks_picker, _| { + tasks_picker + .delegate + .candidates + .iter() + .flatten() + .find(|(_, task)| task.resolved_label == scheduled_task_label) + .cloned() + .unwrap() + }); + project.update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, _| { + let (kind, task) = scheduled_task; + inventory.task_scheduled(kind, task); + }) + }); + tasks_picker.update(cx, |_, cx| { + cx.emit(DismissEvent); + }); + drop(tasks_picker); + cx.executor().run_until_parked() + } + fn open_spawn_tasks( workspace: &View, cx: &mut VisualTestContext, From 74241d9f935158dc113040c3d2c8b3785fdf0f09 Mon Sep 17 00:00:00 2001 From: Lachlan Campbell Date: Mon, 22 Apr 2024 05:37:05 -0400 Subject: [PATCH 003/101] Add woff(2) to file type icon list (#10833) I noticed the sidebar was using the fallback icons for woff/woff2 webfont files, instead of the font icon: CleanShot 2024-04-22 at 03 01 18@2x With this PR, I'm hoping all those font files would use the A icon instead. Release Notes: - Updated`.woff` & `.woff2` file types in the sidebar to display the font icon. --- assets/icons/file_icons/file_types.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 25aa6c2aa0..5a587b02cf 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -161,6 +161,8 @@ "webp": "image", "wma": "audio", "wmv": "video", + "woff": "font", + "woff2": "font", "wv": "audio", "xls": "document", "xlsx": "document", From 615de381dabd3501ff8863a201c2df1be1b70b46 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:38:16 +0200 Subject: [PATCH 004/101] terminal: hide navigation buttons (#10847) We were effectively discarding value set by display_nav_history_buttons once we've updated settings for a pane. This commit adds another bit of state to display_nav_history_buttons by allowing it to hard-deny setting updates. Release Notes: - Fixed a bug that caused disabled navigation buttons to show up in terminal panel. --- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/workspace/src/pane.rs | 72 ++++++++++++---------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 475e670cea..6f3fe0a15c 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -71,7 +71,7 @@ impl TerminalPanel { ); pane.set_can_split(false, cx); pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(false); + pane.display_nav_history_buttons(None); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { let terminal_panel = terminal_panel.clone(); h_flex() diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 58b1d10aa0..c2d2b79961 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -206,7 +206,9 @@ pub struct Pane { render_tab_bar_buttons: Rc) -> AnyElement>, _subscriptions: Vec, tab_bar_scroll_handle: ScrollHandle, - display_nav_history_buttons: bool, + /// Is None if navigation buttons are permanently turned off (and should not react to setting changes). + /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed. + display_nav_history_buttons: Option, double_click_dispatch_action: Box, } @@ -378,7 +380,9 @@ impl Pane { }) .into_any_element() }), - display_nav_history_buttons: TabBarSettings::get_global(cx).show_nav_history_buttons, + display_nav_history_buttons: Some( + TabBarSettings::get_global(cx).show_nav_history_buttons, + ), _subscriptions: subscriptions, double_click_dispatch_action, } @@ -447,8 +451,9 @@ impl Pane { } fn settings_changed(&mut self, cx: &mut ViewContext) { - self.display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons; - + 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; + } if !PreviewTabsSettings::get_global(cx).enabled { self.preview_item_id = None; } @@ -1663,32 +1668,37 @@ impl Pane { fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement { TabBar::new("tab_bar") .track_scroll(self.tab_bar_scroll_handle.clone()) - .when(self.display_nav_history_buttons, |tab_bar| { - tab_bar.start_child( - h_flex() - .gap_2() - .child( - IconButton::new("navigate_backward", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_backward()) - .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)), - ) - .child( - IconButton::new("navigate_forward", IconName::ArrowRight) - .icon_size(IconSize::Small) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_forward) - }) - .disabled(!self.can_navigate_forward()) - .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)), - ), - ) - }) + .when( + self.display_nav_history_buttons.unwrap_or_default(), + |tab_bar| { + tab_bar.start_child( + h_flex() + .gap_2() + .child( + IconButton::new("navigate_backward", IconName::ArrowLeft) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_backward()) + .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)), + ) + .child( + IconButton::new("navigate_forward", IconName::ArrowRight) + .icon_size(IconSize::Small) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_forward) + }) + .disabled(!self.can_navigate_forward()) + .tooltip(|cx| { + Tooltip::for_action("Go Forward", &GoForward, cx) + }), + ), + ) + }, + ) .when(self.has_focus(cx), |tab_bar| { tab_bar.end_child({ let render_tab_buttons = self.render_tab_bar_buttons.clone(); @@ -1921,7 +1931,7 @@ impl Pane { .log_err(); } - pub fn display_nav_history_buttons(&mut self, display: bool) { + pub fn display_nav_history_buttons(&mut self, display: Option) { self.display_nav_history_buttons = display; } } From 67e7c33428ed4d7f0b701b10c17ea0014eab62ef Mon Sep 17 00:00:00 2001 From: Karolis Narkevicius Date: Mon, 22 Apr 2024 16:22:51 +0100 Subject: [PATCH 005/101] Add ReScript suggested extension for res and resi files (#10822) Release Notes: - Added ReScript as a suggested extension for .res and .resi files --- crates/extensions_ui/src/extension_suggest.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index f6d4083cdc..ebc889dcff 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -56,6 +56,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("purescript", &["purs"]), ("r", &["r", "R"]), ("racket", &["rkt"]), + ("rescript", &["res", "resi"]), ("sql", &["sql"]), ("scheme", &["scm"]), ("svelte", &["svelte"]), From ee531b6f4dd2090472b8aff75bf77eb9b2d6e250 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 11:41:16 -0400 Subject: [PATCH 006/101] Sort the list of suggested extensions (#10854) This PR sorts the list of suggested extensions. Release Notes: - N/A --- crates/extensions_ui/src/extension_suggest.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index ebc889dcff..d4699fc798 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -45,8 +45,8 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("java", &["java"]), ("kotlin", &["kt"]), ("latex", &["tex"]), - ("lua", &["lua"]), ("log", &["log"]), + ("lua", &["lua"]), ("make", &["Makefile"]), ("nix", &["nix"]), ("nu", &["nu"]), @@ -57,8 +57,8 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("r", &["r", "R"]), ("racket", &["rkt"]), ("rescript", &["res", "resi"]), - ("sql", &["sql"]), ("scheme", &["scm"]), + ("sql", &["sql"]), ("svelte", &["svelte"]), ("swift", &["swift"]), ("templ", &["templ"]), From 189cece03efadc6cbec87eac53a71b4553c7a092 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 22 Apr 2024 11:51:06 -0400 Subject: [PATCH 007/101] Add analyze highlights script (#10855) Adds a script to print all unique highlight keys for building syntax themes. Usage: - `python script/analyze_highlights.py` OR - `python script/analyze_highlights.py -v` - Using the `-v` or `--verbose` arg will print each language that uses each key. Example output: ``` @attribute (6) @boolean (5) @charset (1) @comment (19) @comment.doc (3) @comment.unused (2) @constant (27) @constant.builtin (15) @constant.character (1) @constructor (4) @embedded (10) @emphasis (1) @emphasis.strong (1) @escape (4) @function (44) @function.builtin (2) @function.definition (2) @function.method (22) @function.method.builtin (3) @function.special (4) @function.special.definition (1) @import (1) @keyframes (1) @keyword (32) @label (2) @link_text (1) @link_uri (1) @media (1) @module (1) @namespace (1) @number (16) @operator (24) @property (11) @property.json_key (1) @punctuation (1) @punctuation.bracket (28) @punctuation.delimiter (12) @punctuation.list_marker (1) @punctuation.special (17) @string (23) @string.doc (1) @string.escape (5) @string.regex (7) @string.special (4) @string.special.symbol (2) @supports (1) @tag (14) @text.literal (2) @title (1) @type (28) @type.builtin (4) @type.super (3) @variable (5) @variable.member (3) @variable.parameter (4) @variable.special (12) Extension-only: @tag.delimiter (1) ``` Verbose example output: ``` Shared: @attribute (6) - [css, heex, javascript, tsx] @boolean (5) - [javascript, proto, tsx, typescript, yaml] @charset (1) - [css] @comment (19) - [bash, c, cpp, css, elixir, erb, go, gomod, gowork, heex, javascript, json, proto, python, ruby, rust, tsx, typescript, yaml] @comment.doc (3) - [elixir] @comment.unused (2) - [elixir] @constant (27) - [bash, c, cpp, elixir, heex, javascript, json, proto, python, ruby, rust, tsx, typescript] @constant.builtin (15) - [elixir, go, javascript, python, ruby, tsx, typescript, yaml] @constant.character (1) - [regex] @constructor (4) - [tsx, typescript] @embedded (10) - [bash, elixir, javascript, python, ruby, tsx, typescript] @emphasis (1) - [markdown] @emphasis.strong (1) - [markdown] @escape (4) - [go, python, regex, ruby] @function (44) - [bash, c, cpp, css, elixir, go, heex, javascript, python, rust, tsx, typescript] @function.builtin (2) - [python] @function.definition (2) - [rust] @function.method (22) - [go, javascript, python, ruby, rust, tsx, typescript] @function.method.builtin (3) - [ruby] @function.special (4) - [c, cpp, rust] @function.special.definition (1) - [rust] @import (1) - [css] @keyframes (1) - [css] @keyword (32) - [bash, c, cpp, css, elixir, erb, go, gomod, gowork, heex, javascript, jsdoc, proto, python, ruby, rust, tsx, typescript] @label (2) - [c, cpp] @link_text (1) - [markdown] @link_uri (1) - [markdown] @media (1) - [css] @module (1) - [heex] @namespace (1) - [css] @number (16) - [bash, c, cpp, css, elixir, go, javascript, json, proto, python, regex, ruby, rust, tsx, typescript, yaml] @operator (24) - [bash, c, cpp, css, elixir, go, gomod, gowork, heex, javascript, proto, python, regex, ruby, tsx, typescript] @property (11) - [bash, c, cpp, css, javascript, python, regex, rust, tsx, typescript, yaml] @property.json_key (1) - [json] @punctuation (1) - [elixir] @punctuation.bracket (28) - [c, cpp, elixir, go, heex, javascript, json, proto, regex, ruby, rust, tsx, typescript, yaml] @punctuation.delimiter (12) - [c, cpp, css, elixir, heex, javascript, proto, regex, ruby, tsx, typescript, yaml] @punctuation.list_marker (1) - [markdown] @punctuation.special (17) - [elixir, javascript, python, ruby, tsx, typescript, yaml] @string (23) - [bash, c, cpp, css, elixir, go, gomod, gowork, heex, javascript, json, proto, python, regex, ruby, rust, tsx, typescript, yaml] @string.doc (1) - [python] @string.escape (5) - [elixir, javascript, tsx, typescript, yaml] @string.regex (7) - [elixir, javascript, ruby, tsx, typescript] @string.special (4) - [css, elixir] @string.special.symbol (2) - [elixir, ruby] @supports (1) - [css] @tag (14) - [css, heex, javascript, tsx] @text.literal (2) - [markdown] @title (1) - [markdown] @type (28) - [c, cpp, css, elixir, go, javascript, jsdoc, proto, python, ruby, rust, tsx, typescript, yaml] @type.builtin (4) - [javascript, rust, tsx, typescript] @type.super (3) - [ruby] @variable (5) - [c, cpp, javascript, tsx, typescript] @variable.member (3) - [go, ruby] @variable.parameter (4) - [ruby] @variable.special (12) - [cpp, css, javascript, ruby, rust, tsx, typescript] Extension-only: @tag.delimiter (1) - [astro] ``` Release Notes: - N/A --------- Co-authored-by: Joseph T. Lyons --- script/analyze_highlights.py | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 script/analyze_highlights.py diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py new file mode 100644 index 0000000000..968264a7c7 --- /dev/null +++ b/script/analyze_highlights.py @@ -0,0 +1,68 @@ +""" +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. + +This is useful to help avoid accidentally introducing new tags when appropriate ones already exist when adding new languages. + +Flags: +-v, --verbose: Include a detailed list of languages for each tag found in the highlight.scm files. +""" + +from collections import defaultdict +from pathlib import Path +from typing import Any +import argparse +import re + +pattern = re.compile(r'@(?!_)[a-zA-Z_.]+') + +def parse_arguments(): + parser = argparse.ArgumentParser(description='Analyze highlight.scm files for unique instances and their languages.') + parser.add_argument('-v', '--verbose', action='store_true', help='Include a list of languages for each tag.') + return parser.parse_args() + +def find_highlight_files(root_dir): + for path in Path(root_dir).rglob('highlights.scm'): + yield path + +def count_instances(files): + instances: defaultdict[list[Any], dict[str, Any]] = defaultdict(lambda: {'count': 0, 'languages': set()}) + for file_path in files: + language = file_path.parent.name + with open(file_path, "r") as file: + text = file.read() + matches = pattern.findall(text) + for match in matches: + instances[match]['count'] += 1 + instances[match]['languages'].add(language) + return instances + +def print_instances(instances, verbose=False): + for item, details in sorted(instances.items(), key=lambda x: x[0]): + languages = ', '.join(sorted(details['languages'])) + if verbose: + print(f"{item} ({details['count']}) - [{languages}]") + else: + print(f"{item} ({details['count']})") + +def main(): + args = parse_arguments() + + base_dir = Path(__file__).parent.parent + core_path = base_dir / 'crates/languages/src' + extension_path = base_dir / 'extensions/astro/languages' + + core_instances = count_instances(find_highlight_files(core_path)) + extension_instances = count_instances(find_highlight_files(extension_path)) + + unique_extension_instances = {k: v for k, v in extension_instances.items() if k not in core_instances} + + print('Shared:\n') + print_instances(core_instances, args.verbose) + + if unique_extension_instances: + print('\nExtension-only:\n') + print_instances(unique_extension_instances, args.verbose) + +if __name__ == '__main__': + main() From a111b959d282d643d022b3c17b9f5ebba5aa8094 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:01:06 +0200 Subject: [PATCH 008/101] cli: Treat first argument as name of release channel to use for the cli (#10856) With this commit, it is now possible to invoke cli with a release channel of bundle as an argument. E.g: `zed stable some_arguments` will find CLI binary of Stable channel installed on your machine and invoke it with `some_arguments` (so the first argument is essentially omitted). Fixes #10851 Release Notes: - CLI now accepts an optional name of release channel as it's first argument. For example, `zed stable` will always use your Stable installation's CLI. Trailing args are passed along. --- Cargo.lock | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/main.rs | 43 +++++++++++++++++++++++++++++-- crates/release_channel/src/lib.rs | 27 ++++++++++++++----- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b243f7d59..e8d019f3ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,6 +2047,7 @@ dependencies = [ "core-services", "ipc-channel", "plist", + "release_channel", "serde", "util", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index d118d9d873..5b982e9bfb 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" anyhow.workspace = true clap.workspace = true ipc-channel = "0.18" +release_channel.workspace = true serde.workspace = true util.workspace = true diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 9ae0ed0465..80c892583f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -7,7 +7,7 @@ use serde::Deserialize; use std::{ env, ffi::OsStr, - fs::{self}, + fs, path::{Path, PathBuf}, }; use util::paths::PathLikeWithPosition; @@ -53,6 +53,16 @@ struct InfoPlist { } fn main() -> Result<()> { + // Intercept version designators + #[cfg(target_os = "macos")] + if let Some(channel) = std::env::args().nth(1) { + // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along. + use std::str::FromStr as _; + + if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel) { + return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); + } + } let args = Args::parse(); let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?; @@ -200,7 +210,7 @@ mod windows { #[cfg(target_os = "macos")] mod mac_os { - use anyhow::Context; + use anyhow::{Context, Result}; use core_foundation::{ array::{CFArray, CFIndex}, string::kCFStringEncodingUTF8, @@ -348,4 +358,33 @@ mod mac_os { ) } } + pub(super) fn spawn_channel_cli( + channel: release_channel::ReleaseChannel, + leftover_args: Vec, + ) -> Result<()> { + use anyhow::bail; + use std::process::Command; + + let app_id_prompt = format!("id of app \"{}\"", channel.display_name()); + let app_id_output = Command::new("osascript") + .arg("-e") + .arg(&app_id_prompt) + .output()?; + if !app_id_output.status.success() { + bail!("Could not determine app id for {}", channel.display_name()); + } + let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned(); + let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'"); + let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?; + if !app_path_output.status.success() { + bail!( + "Could not determine app path for {}", + channel.display_name() + ); + } + let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned(); + let cli_path = format!("{app_path}/Contents/MacOS/cli"); + Command::new(cli_path).args(leftover_args).spawn()?; + Ok(()) + } } diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs index 864df387c0..5418d4f22e 100644 --- a/crates/release_channel/src/lib.rs +++ b/crates/release_channel/src/lib.rs @@ -2,7 +2,7 @@ #![deny(missing_docs)] -use std::env; +use std::{env, str::FromStr}; use gpui::{AppContext, Global, SemanticVersion}; use once_cell::sync::Lazy; @@ -18,11 +18,8 @@ static RELEASE_CHANNEL_NAME: Lazy = if cfg!(debug_assertions) { #[doc(hidden)] pub static RELEASE_CHANNEL: Lazy = - Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() { - "dev" => ReleaseChannel::Dev, - "nightly" => ReleaseChannel::Nightly, - "preview" => ReleaseChannel::Preview, - "stable" => ReleaseChannel::Stable, + Lazy::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) { + Ok(channel) => channel, _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }); @@ -149,3 +146,21 @@ impl ReleaseChannel { } } } + +/// Error indicating that release channel string does not match any known release channel names. +#[derive(Copy, Clone, Debug, Hash, PartialEq)] +pub struct InvalidReleaseChannel; + +impl FromStr for ReleaseChannel { + type Err = InvalidReleaseChannel; + + fn from_str(channel: &str) -> Result { + Ok(match channel { + "dev" => ReleaseChannel::Dev, + "nightly" => ReleaseChannel::Nightly, + "preview" => ReleaseChannel::Preview, + "stable" => ReleaseChannel::Stable, + _ => return Err(InvalidReleaseChannel), + }) + } +} From d298df823f000538c6766a510322480129dcf0ad Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 22 Apr 2024 13:05:41 -0400 Subject: [PATCH 009/101] Minor script fix (#10857) Fixes a minor error in the analyze highlight script. Release Notes: - N/A --- script/analyze_highlights.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py index 968264a7c7..1fd16f2c0f 100644 --- a/script/analyze_highlights.py +++ b/script/analyze_highlights.py @@ -50,7 +50,7 @@ def main(): base_dir = Path(__file__).parent.parent core_path = base_dir / 'crates/languages/src' - extension_path = base_dir / 'extensions/astro/languages' + extension_path = base_dir / 'extensions/' core_instances = count_instances(find_highlight_files(core_path)) extension_instances = count_instances(find_highlight_files(extension_path)) From 1be452744a7bc3b94a261162c67b85094fb9a98b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:26:49 +0200 Subject: [PATCH 010/101] cli: Use leading dashes in channel designators (#10858) /cc @maxbrunsfeld Release Notes: - N/A --- crates/cli/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 80c892583f..1f4568e0f9 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -55,11 +55,11 @@ struct InfoPlist { fn main() -> Result<()> { // Intercept version designators #[cfg(target_os = "macos")] - if let Some(channel) = std::env::args().nth(1) { + if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along. use std::str::FromStr as _; - if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel) { + if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect()); } } From b964fe2ccf1e6d24a20f8d86feea69d325151838 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 14:40:23 -0400 Subject: [PATCH 011/101] Fix reading workspace-level LSP settings in extensions (#10859) This PR fixes an issue where workspace-level LSP settings could be not read using `LspSettings::for_worktree` in extensions. We we erroneously always reading the global settings instead of respecting the passed-in location. Release Notes: - Fixed a bug where workspace LSP settings could not be read by extensions. --- crates/extension/src/wasm_host/wit/since_v0_0_6.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs index 2c201169a0..7d3c7b6380 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs +++ b/crates/extension/src/wasm_host/wit/since_v0_0_6.rs @@ -227,7 +227,7 @@ impl ExtensionImports for WasmState { "lsp" => { let settings = key .and_then(|key| { - ProjectSettings::get_global(cx) + ProjectSettings::get(location, cx) .lsp .get(&Arc::::from(key)) }) From e9a965fe8162d7247e5a01f8547607c2f0eaaee2 Mon Sep 17 00:00:00 2001 From: Max Linke Date: Mon, 22 Apr 2024 21:05:12 +0200 Subject: [PATCH 012/101] Add missing linux dependencies (#10814) At least one of the dependencies requires cmake to configure the build process. On ubuntu libgit2-dev was missing. Debian and derivates do not install development headers by default. Release Notes: - Improved Linux development setup scripts. Co-authored-by: Max Linke --- script/linux | 1 + 1 file changed, 1 insertion(+) diff --git a/script/linux b/script/linux index dc35b1bd68..56a000692e 100755 --- a/script/linux +++ b/script/linux @@ -20,6 +20,7 @@ if [[ -n $apt ]]; then libssl-dev libzstd-dev libvulkan1 + libgit2-dev ) $maysudo "$apt" install -y "${deps[@]}" exit 0 From b29643168c32c11294ec7ece53efdc430f9ede36 Mon Sep 17 00:00:00 2001 From: ElKowar Date: Mon, 22 Apr 2024 21:42:18 +0200 Subject: [PATCH 013/101] XDG_BASE_DIR support (linux, windows) (#10808) This PR adds XDG_BASE_DIR support on linux, and cleans up the path declarations slightly. Additionally, we move the embeddings and conversations directly to the SUPPORT_DIR on those platforms. I _think_ that should also be done on MacOS in the future, but that has been left out here for now to not break existing users setups. Additionally, we move the SUPPORT_DIR into LocalAppData on windows for consistency. Release Notes: - Fixed missing support of `XDG_BASE_DIR` on linux - Fixed improper placement of data in XDG_CONFIG_HOME on linux and windows (https://github.com/zed-industries/zed/issues/9308, https://github.com/zed-industries/zed/issues/7155) --------- Co-authored-by: phisch --- crates/util/src/paths.rs | 67 +++++++++++++++++++++++----------------- crates/zed/src/main.rs | 6 +++- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 97740fb75c..205ea72f0a 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -12,49 +12,54 @@ lazy_static::lazy_static! { dirs::config_dir() .expect("failed to determine RoamingAppData directory") .join("Zed") + } else if cfg!(target_os = "linux") { + dirs::config_dir() + .expect("failed to determine XDG_CONFIG_HOME directory") + .join("zed") } else { HOME.join(".config").join("zed") }; - pub static ref CONVERSATIONS_DIR: PathBuf = CONFIG_DIR.join("conversations"); - pub static ref EMBEDDINGS_DIR: PathBuf = CONFIG_DIR.join("embeddings"); + pub static ref CONVERSATIONS_DIR: PathBuf = if cfg!(target_os = "macos") { + CONFIG_DIR.join("conversations") + } else { + SUPPORT_DIR.join("conversations") + }; + pub static ref EMBEDDINGS_DIR: PathBuf = if cfg!(target_os = "macos") { + CONFIG_DIR.join("embeddings") + } else { + SUPPORT_DIR.join("embeddings") + }; pub static ref THEMES_DIR: PathBuf = CONFIG_DIR.join("themes"); - pub static ref LOGS_DIR: PathBuf = if cfg!(target_os = "macos") { - HOME.join("Library/Logs/Zed") + + pub static ref SUPPORT_DIR: PathBuf = if cfg!(target_os = "macos") { + HOME.join("Library/Application Support/Zed") + } else if cfg!(target_os = "linux") { + dirs::data_local_dir() + .expect("failed to determine XDG_DATA_DIR directory") + .join("zed") } else if cfg!(target_os = "windows") { dirs::data_local_dir() .expect("failed to determine LocalAppData directory") - .join("Zed/logs") - } else { - CONFIG_DIR.join("logs") - }; - pub static ref SUPPORT_DIR: PathBuf = if cfg!(target_os = "macos") { - HOME.join("Library/Application Support/Zed") - } else if cfg!(target_os = "windows") { - dirs::config_dir() - .expect("failed to determine RoamingAppData directory") .join("Zed") } else { CONFIG_DIR.clone() }; + pub static ref LOGS_DIR: PathBuf = if cfg!(target_os = "macos") { + HOME.join("Library/Logs/Zed") + } else { + SUPPORT_DIR.join("logs") + }; pub static ref EXTENSIONS_DIR: PathBuf = SUPPORT_DIR.join("extensions"); pub static ref LANGUAGES_DIR: PathBuf = SUPPORT_DIR.join("languages"); pub static ref COPILOT_DIR: PathBuf = SUPPORT_DIR.join("copilot"); pub static ref DEFAULT_PRETTIER_DIR: PathBuf = SUPPORT_DIR.join("prettier"); pub static ref DB_DIR: PathBuf = SUPPORT_DIR.join("db"); - pub static ref CRASHES_DIR: PathBuf = if cfg!(target_os = "macos") { - HOME.join("Library/Logs/DiagnosticReports") - } else if cfg!(target_os = "windows") { - dirs::data_local_dir() - .expect("failed to determine LocalAppData directory") - .join("Zed/crashes") - } else { - CONFIG_DIR.join("crashes") - }; - pub static ref CRASHES_RETIRED_DIR: PathBuf = if cfg!(target_os = "macos") { - HOME.join("Library/Logs/DiagnosticReports/Retired") - } else { - CRASHES_DIR.join("retired") - }; + pub static ref CRASHES_DIR: Option = cfg!(target_os = "macos") + .then_some(HOME.join("Library/Logs/DiagnosticReports")); + pub static ref CRASHES_RETIRED_DIR: Option = CRASHES_DIR + .as_ref() + .map(|dir| dir.join("Retired")); + pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); pub static ref TASKS: PathBuf = CONFIG_DIR.join("tasks.json"); @@ -65,9 +70,13 @@ lazy_static::lazy_static! { pub static ref LOCAL_TASKS_RELATIVE_PATH: &'static Path = Path::new(".zed/tasks.json"); pub static ref LOCAL_VSCODE_TASKS_RELATIVE_PATH: &'static Path = Path::new(".vscode/tasks.json"); pub static ref TEMP_DIR: PathBuf = if cfg!(target_os = "widows") { - dirs::data_local_dir() + dirs::cache_dir() .expect("failed to determine LocalAppData directory") - .join("Temp/Zed") + .join("Zed") + } else if cfg!(target_os = "linux") { + dirs::cache_dir() + .expect("failed to determine XDG_CACHE_HOME directory") + .join("zed") } else { HOME.join(".cache").join("zed") }; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7c2111ba82..d591b0525d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -925,7 +925,11 @@ async fn upload_previous_crashes( let crash_report_url = http.build_zed_api_url("/telemetry/crashes", &[])?; - for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] { + // crash directories are only set on MacOS + for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] + .iter() + .filter_map(|d| d.as_deref()) + { let mut children = smol::fs::read_dir(&dir).await?; while let Some(child) = children.next().await { let child = child?; From 07f490f9e959165edde0cf01ca5719be4a04d299 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 15:56:54 -0400 Subject: [PATCH 014/101] Ensure `target` directory exists before drafting release notes --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c4864fa32..7fa313d5ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,6 +205,7 @@ jobs: echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" exit 1 fi + mkdir -p target/ script/draft-release-notes "$version" "$channel" > target/release-notes.md - name: Generate license file From 33baa377c700cd2217cccdcfe92f3b5eff744c38 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 15:58:25 -0400 Subject: [PATCH 015/101] docs: Add note about manually using the `bump_patch_version` action (#10862) This PR updates the releases docs to make a note about `bump_patch_version` action through the GitHub UI. Not all of us have `gh` (or `brew`) installed. Release Notes: - N/A --- docs/src/developing_zed__releases.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/developing_zed__releases.md b/docs/src/developing_zed__releases.md index f57aa794a2..ecf01fdc9e 100644 --- a/docs/src/developing_zed__releases.md +++ b/docs/src/developing_zed__releases.md @@ -33,9 +33,11 @@ If your PR fixes a panic or a crash, you should cherry-pick it to the current st You will need write access to the Zed repository to do this: - Send a PR containing your change to `main` as normal. -- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the Github bot will send a PR to the branch. +- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the GitHub bot will send a PR to the branch. - In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch. - After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build. + - This can also be run from the [GitHub Actions UI](https://github.com/zed-industries/zed/actions/workflows/bump_patch_version.yml): + ![](https://github.com/zed-industries/zed/assets/1486634/9e31ae95-09e1-4c7f-9591-944f4f5b63ea) - Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes) - Proof-read and edit the release notes as needed. - Download the artifacts for each release and test that you can run them locally. From 7f81bfb6b7ad3dfecc5df67b8ce6519f3179fdc8 Mon Sep 17 00:00:00 2001 From: ElKowar Date: Mon, 22 Apr 2024 22:24:25 +0200 Subject: [PATCH 016/101] Make keymaps reusable across platforms (#10811) This PR includes two relevant changes: - Platform binds (super, windows, cmd) will now parse on all platforms, regardless of which one is being used. While very counter-intuitive (this means that `cmd-d` will actually be triggered by `win-d` on windows) this makes it possible to reuse keymap files across platforms easily - There is now a KeyContext `os == linux`, `os == macos` or `os == windows` available in keymaps. This allows users to specify certain blocks of keybinds only for one OS, allowing you to minimize the amount of keymappings that you have to re-configure for each platform. Release Notes: - Added `os` KeyContext, set to either `linux`, `macos` or `windows` - Fixed keymap parsing errors when `cmd` was used on linux, `super` was used on mac, etc. --- crates/editor/src/editor.rs | 2 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/gpui/src/keymap/context.rs | 14 ++++++++++++++ crates/gpui/src/platform/keystroke.rs | 7 +------ crates/project_panel/src/project_panel.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/vim/src/state.rs | 2 +- crates/workspace/src/dock.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 11 files changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3356d507cd..6cd9680d0e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1550,7 +1550,7 @@ impl Editor { } fn key_context(&self, cx: &AppContext) -> KeyContext { - let mut key_context = KeyContext::default(); + let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { EditorMode::SingleLine => "single_line", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 41d82a1405..25ef796784 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -700,7 +700,7 @@ impl ExtensionsPage { } fn render_search(&self, cx: &mut ViewContext) -> Div { - let mut key_context = KeyContext::default(); + let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); let editor_border = if self.query_contains_error { diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 6c22fa9fd6..6ac22d2162 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -25,6 +25,20 @@ impl<'a> TryFrom<&'a str> for KeyContext { } impl KeyContext { + /// Initialize a new [`KeyContext`] that contains an `os` key set to either `macos`, `linux`, `windows` or `unknown`. + pub fn new_with_defaults() -> Self { + let mut context = Self::default(); + #[cfg(target_os = "macos")] + context.set("os", "macos"); + #[cfg(target_os = "linux")] + context.set("os", "linux"); + #[cfg(target_os = "windows")] + context.set("os", "windows"); + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + context.set("os", "unknown"); + context + } + /// Parse a key context from a string. /// The key context format is very simple: /// - either a single identifier, such as `StatusBar` diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 5f3d75f72e..55f8658cd3 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -74,12 +74,7 @@ impl Keystroke { "alt" => alt = true, "shift" => shift = true, "fn" => function = true, - #[cfg(target_os = "macos")] - "cmd" => platform = true, - #[cfg(target_os = "linux")] - "super" => platform = true, - #[cfg(target_os = "windows")] - "win" => platform = true, + "cmd" | "super" | "win" => platform = true, _ => { if let Some(next) = components.peek() { if next.is_empty() && source.ends_with('-') { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b08f46b7e7..f592103b20 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1698,7 +1698,7 @@ impl ProjectPanel { } fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { - let mut dispatch_context = KeyContext::default(); + let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("ProjectPanel"); dispatch_context.add("menu"); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 71ba5a65c3..0e0c33c265 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -188,7 +188,7 @@ impl Render for BufferSearchBar { let should_show_replace_input = self.replace_enabled && supported_options.replacement; let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); - let mut key_context = KeyContext::default(); + let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); if in_replace { key_context.add("in_replace"); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cc5c54eca9..6c3908305f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -348,7 +348,7 @@ impl TerminalView { } fn dispatch_context(&self, cx: &AppContext) -> KeyContext { - let mut dispatch_context = KeyContext::default(); + let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("Terminal"); let mode = self.terminal.read(cx).last_content.mode; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 82d4fc9af3..9ece818b16 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -194,7 +194,7 @@ impl EditorState { } pub fn keymap_context_layer(&self) -> KeyContext { - let mut context = KeyContext::default(); + let mut context = KeyContext::new_with_defaults(); context.set( "vim_mode", match self.mode { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 4dad415445..e6ece91f96 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -552,7 +552,7 @@ impl Dock { } fn dispatch_context() -> KeyContext { - let mut dispatch_context = KeyContext::default(); + let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("Dock"); dispatch_context diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c2d2b79961..de561320fa 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1944,7 +1944,7 @@ impl FocusableView for Pane { impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut key_context = KeyContext::default(); + let mut key_context = KeyContext::new_with_defaults(); key_context.add("Pane"); if self.active_item().is_none() { key_context.add("EmptyPane"); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9c6e07b941..303323f735 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3945,7 +3945,7 @@ struct DraggedDock(DockPosition); impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut context = KeyContext::default(); + let mut context = KeyContext::new_with_defaults(); context.add("Workspace"); let centered_layout = self.centered_layout && self.center.panes().len() == 1 From c96a96b3ce538f452b677f4f08730aed309162dd Mon Sep 17 00:00:00 2001 From: Ben Hamment Date: Mon, 22 Apr 2024 21:43:48 +0100 Subject: [PATCH 017/101] Add traits in Rust highlights (#10731) Question: I use type.super here because I made a similar change to the ruby syntax to apply the same style to superclasses. With this in mind, should this change be renamed to type.trait or should it be renamed to something like type.italic so the ruby syntax or any other language can all use type.italic? or maybe something else altogether. image Release Notes: - Exposed Rust traits as `type.interface` for individual syntax theming. --- crates/languages/src/rust/highlights.scm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index e01010151f..79a50185aa 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -3,6 +3,12 @@ (self) @variable.special (field_identifier) @property +(trait_item name: (type_identifier) @type.interface) +(impl_item trait: (type_identifier) @type.interface) +(abstract_type trait: (type_identifier) @type.interface) +(dynamic_type trait: (type_identifier) @type.interface) +(trait_bounds (type_identifier) @type.interface) + (call_expression function: [ (identifier) @function From 63c529552c0920cf03f9dba3ea4bb2f422725f00 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 18:02:22 -0400 Subject: [PATCH 018/101] Automatically install the HTML extension (#10867) This PR makes it so the HTML extension will be installed in Zed by default. We feel we should keep HTML available out-of-the-box, but we want to do so while still keeping it as an extension (as opposed to built-in to Zed natively). There may be a world where we bundle the extension in with the Zed binary itself, but installing it on startup gets us 99% of the way there. The approach for making HTML available by default is quite general, and could be applied to any extension that we choose (likely other languages that we want to come out-of-the-box, but that could then be moved to extensions). If you do not want the HTML extension in Zed, you can disable the auto-installation in your `settings.json` and then uninstall the extension: ```json { "auto_install_extensions": { "html": false } } ``` Release Notes: - Added auto-installation for the HTML extension on startup. - This can be disabled by adding `{ "auto_install_extensions": { "html": false } }` to your settings. --- assets/settings/default.json | 7 +++++ crates/extension/src/extension_settings.rs | 21 ++++++++++++- crates/extension/src/extension_store.rs | 34 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5c950e2469..c5ee98abf0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -590,6 +590,13 @@ // } // "file_types": {}, + // The extensions that Zed should automatically install on startup. + // + // If you don't want any of these extensions, add this field to your settings + // and change the value to `false`. + "auto_install_extensions": { + "html": true + }, // Different settings for specific languages. "languages": { "C++": { diff --git a/crates/extension/src/extension_settings.rs b/crates/extension/src/extension_settings.rs index 42ee34930c..c11a6622b8 100644 --- a/crates/extension/src/extension_settings.rs +++ b/crates/extension/src/extension_settings.rs @@ -5,14 +5,29 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; use std::sync::Arc; +use util::merge_non_null_json_value_into; #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct ExtensionSettings { + /// The extensions that should be automatically installed by Zed. + /// + /// This is used to make functionality provided by extensions (e.g., language support) + /// available out-of-the-box. + #[serde(default)] + pub auto_install_extensions: HashMap, bool>, #[serde(default)] pub auto_update_extensions: HashMap, bool>, } impl ExtensionSettings { + /// Returns whether the given extension should be auto-installed. + pub fn should_auto_install(&self, extension_id: &str) -> bool { + self.auto_install_extensions + .get(extension_id) + .copied() + .unwrap_or(true) + } + pub fn should_auto_update(&self, extension_id: &str) -> bool { self.auto_update_extensions .get(extension_id) @@ -27,6 +42,10 @@ impl Settings for ExtensionSettings { type FileContent = Self; fn load(sources: SettingsSources, _cx: &mut AppContext) -> Result { - Ok(sources.user.cloned().unwrap_or_default()) + let mut merged = serde_json::Value::Null; + for value in [sources.default].into_iter().chain(sources.user) { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) } } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index a4bdcb215e..f6bd040c53 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -291,6 +291,8 @@ impl ExtensionStore { if let Some(future) = reload_future { future.await; } + this.update(&mut cx, |this, cx| this.auto_install_extensions(cx)) + .ok(); this.update(&mut cx, |this, cx| this.check_for_updates(cx)) .ok(); }) @@ -480,6 +482,38 @@ impl ExtensionStore { self.fetch_extensions_from_api(&format!("/extensions/{extension_id}"), &[], cx) } + /// Installs any extensions that should be included with Zed by default. + /// + /// This can be used to make certain functionality provided by extensions + /// available out-of-the-box. + pub fn auto_install_extensions(&mut self, cx: &mut ModelContext) { + let extension_settings = ExtensionSettings::get_global(cx); + + let extensions_to_install = extension_settings + .auto_install_extensions + .keys() + .filter(|extension_id| extension_settings.should_auto_install(extension_id)) + .filter(|extension_id| { + let is_already_installed = self + .extension_index + .extensions + .contains_key(extension_id.as_ref()); + !is_already_installed + }) + .cloned() + .collect::>(); + + cx.spawn(move |this, mut cx| async move { + for extension_id in extensions_to_install { + this.update(&mut cx, |this, cx| { + this.install_latest_extension(extension_id.clone(), cx); + }) + .ok(); + } + }) + .detach(); + } + pub fn check_for_updates(&mut self, cx: &mut ModelContext) { let task = self.fetch_extensions_with_update_available(cx); cx.spawn(move |this, mut cx| async move { From 029eb6704380643ddac21928f48d5bf7e9bb8cb7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 22 Apr 2024 18:38:34 -0400 Subject: [PATCH 019/101] Add `SettingsSources::::json_merge_with` function (#10869) This PR adds a `json_merge_with` function to `SettingsSources::` to allow JSON merging settings from custom sources. This should help avoid repeating the actual merging logic when all that needs to be customized is which sources are being respected. Release Notes: - N/A --- crates/extension/src/extension_settings.rs | 9 +++------ crates/settings/src/settings_store.rs | 21 +++++++++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/extension/src/extension_settings.rs b/crates/extension/src/extension_settings.rs index c11a6622b8..a2ab7ac9cc 100644 --- a/crates/extension/src/extension_settings.rs +++ b/crates/extension/src/extension_settings.rs @@ -5,7 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; use std::sync::Arc; -use util::merge_non_null_json_value_into; #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] pub struct ExtensionSettings { @@ -42,10 +41,8 @@ impl Settings for ExtensionSettings { type FileContent = Self; fn load(sources: SettingsSources, _cx: &mut AppContext) -> Result { - let mut merged = serde_json::Value::Null; - for value in [sources.default].into_iter().chain(sources.user) { - merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); - } - Ok(serde_json::from_value(merged)?) + SettingsSources::::json_merge_with( + [sources.default].into_iter().chain(sources.user), + ) } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index cdc2640092..a6bb5838f9 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -116,16 +116,25 @@ impl<'a, T: Serialize> SettingsSources<'a, T> { .chain(self.project.iter().copied()) } + /// Returns the settings after performing a JSON merge of the provided customizations. + /// + /// Customizations later in the iterator win out over the earlier ones. + pub fn json_merge_with( + customizations: impl Iterator, + ) -> Result { + let mut merged = serde_json::Value::Null; + for value in customizations { + merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); + } + Ok(serde_json::from_value(merged)?) + } + /// Returns the settings after performing a JSON merge of the customizations into the /// default settings. /// /// More-specific customizations win out over the less-specific ones. - pub fn json_merge(&self) -> Result { - let mut merged = serde_json::Value::Null; - for value in self.defaults_and_customizations() { - merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); - } - Ok(serde_json::from_value(merged)?) + pub fn json_merge(&'a self) -> Result { + Self::json_merge_with(self.defaults_and_customizations()) } } From ae3c641bbee2029fb4588d008e45ddb783593622 Mon Sep 17 00:00:00 2001 From: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:20:24 -0300 Subject: [PATCH 020/101] wayland: File drag and drop (#10817) Implements file drag and drop on Wayland https://github.com/zed-industries/zed/assets/71973804/febcfbfe-3a23-4593-8dd3-e85254e58eb5 Release Notes: - N/A --- Cargo.lock | 12 + crates/gpui/Cargo.toml | 1 + crates/gpui/src/platform/linux/platform.rs | 17 ++ .../gpui/src/platform/linux/wayland/client.rs | 253 +++++++++++++++++- 4 files changed, 270 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8d019f3ac..52942e16b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3799,6 +3799,17 @@ dependencies = [ "util", ] +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + [[package]] name = "filetime" version = "0.2.22" @@ -4482,6 +4493,7 @@ dependencies = [ "derive_more", "env_logger", "etagere", + "filedescriptor", "flume", "font-kit", "foreign-types 0.5.0", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 471e0a7347..f49ab6e571 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -115,6 +115,7 @@ wayland-protocols = { version = "0.31.2", features = [ ] } oo7 = "0.3.0" open = "5.1.2" +filedescriptor = "0.8.2" x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] } xkbcommon = { version = "0.7", features = ["wayland", "x11"] } diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 6bf70ad46d..8ecb639335 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -3,7 +3,10 @@ use std::any::{type_name, Any}; use std::cell::{self, RefCell}; use std::env; +use std::fs::File; +use std::io::Read; use std::ops::{Deref, DerefMut}; +use std::os::fd::{AsRawFd, FromRawFd}; use std::panic::Location; use std::{ path::{Path, PathBuf}, @@ -19,6 +22,7 @@ use async_task::Runnable; use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; use copypasta::ClipboardProvider; +use filedescriptor::FileDescriptor; use flume::{Receiver, Sender}; use futures::channel::oneshot; use parking_lot::Mutex; @@ -484,6 +488,19 @@ pub(super) fn is_within_click_distance(a: Point, b: Point) -> bo diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE } +pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result { + let mut file = File::from_raw_fd(fd.as_raw_fd()); + + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + + // Normalize the text to unix line endings, otherwise + // copying from eg: firefox inserts a lot of blank + // lines, and that is super annoying. + let result = buffer.replace("\r\n", "\n"); + Ok(result) +} + impl Keystroke { pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self { let mut modifiers = modifiers; diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 1eb438795f..8f23b22921 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1,5 +1,9 @@ +use core::hash; use std::cell::{RefCell, RefMut}; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::path::PathBuf; use std::rc::{Rc, Weak}; +use std::sync::Arc; use std::time::{Duration, Instant}; use async_task::Runnable; @@ -9,13 +13,19 @@ use calloop_wayland_source::WaylandSource; use collections::HashMap; use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary}; use copypasta::ClipboardProvider; +use filedescriptor::Pipe; +use smallvec::SmallVec; use util::ResultExt; use wayland_backend::client::ObjectId; use wayland_backend::protocol::WEnum; +use wayland_client::event_created_child; use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents}; use wayland_client::protocol::wl_callback::{self, WlCallback}; -use wayland_client::protocol::wl_output; +use wayland_client::protocol::wl_data_device_manager::DndAction; use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource}; +use wayland_client::protocol::{ + wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, +}; use wayland_client::{ delegate_noop, protocol::{ @@ -35,14 +45,14 @@ use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_ba use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS}; -use super::super::DOUBLE_CLICK_INTERVAL; +use super::super::{read_fd, DOUBLE_CLICK_INTERVAL}; use super::window::{WaylandWindowState, WaylandWindowStatePtr}; use crate::platform::linux::is_within_click_distance; use crate::platform::linux::wayland::cursor::Cursor; use crate::platform::linux::wayland::window::WaylandWindow; use crate::platform::linux::LinuxClient; use crate::platform::PlatformWindow; -use crate::{point, px, ForegroundExecutor, MouseExitEvent}; +use crate::{point, px, FileDropEvent, ForegroundExecutor, MouseExitEvent}; use crate::{ AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, @@ -58,6 +68,7 @@ const MIN_KEYCODE: u32 = 8; pub struct Globals { pub qh: QueueHandle, pub compositor: wl_compositor::WlCompositor, + pub data_device_manager: Option, pub wm_base: xdg_wm_base::XdgWmBase, pub shm: wl_shm::WlShm, pub viewporter: Option, @@ -82,6 +93,13 @@ impl Globals { (), ) .unwrap(), + data_device_manager: globals + .bind( + &qh, + WL_DATA_DEVICE_MANAGER_VERSION..=WL_DATA_DEVICE_MANAGER_VERSION, + (), + ) + .ok(), shm: globals.bind(&qh, 1..=1, ()).unwrap(), wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), viewporter: globals.bind(&qh, 1..=1, ()).ok(), @@ -94,13 +112,16 @@ impl Globals { } pub(crate) struct WaylandClientState { + serial: u32, globals: Globals, wl_pointer: Option, + data_device: Option, // Surface to Window mapping windows: HashMap, // Output to scale mapping output_scales: HashMap, keymap_state: Option, + drag: DragState, click: ClickState, repeat: KeyRepeat, modifiers: Modifiers, @@ -124,6 +145,12 @@ pub(crate) struct WaylandClientState { common: LinuxCommon, } +pub struct DragState { + data_offer: Option, + window: Option, + position: Point, +} + pub struct ClickState { last_click: Instant, last_location: Point, @@ -167,6 +194,12 @@ impl WaylandClientStatePtr { // Drop the clipboard to prevent a seg fault after we've closed all Wayland connections. state.clipboard = None; state.primary = None; + if let Some(wl_pointer) = &state.wl_pointer { + wl_pointer.release(); + } + if let Some(data_device) = &state.data_device { + data_device.release(); + } state.common.signal.stop(); } } @@ -175,6 +208,7 @@ impl WaylandClientStatePtr { #[derive(Clone)] pub struct WaylandClient(Rc>); +const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; const WL_OUTPUT_VERSION: u32 = 2; fn wl_seat_version(version: u32) -> u32 { @@ -199,18 +233,20 @@ impl WaylandClient { let (globals, mut event_queue) = registry_queue_init::(&conn).unwrap(); let qh = event_queue.handle(); - let mut outputs = HashMap::default(); + let mut seat: Option = None; + let mut outputs = HashMap::default(); globals.contents().with_list(|list| { for global in list { match &global.interface[..] { "wl_seat" => { - globals.registry().bind::( + // TODO: multi-seat support + seat = Some(globals.registry().bind::( global.name, wl_seat_version(global.version), &qh, (), - ); + )); } "wl_output" => { let output = globals.registry().bind::( @@ -227,34 +263,47 @@ impl WaylandClient { }); let display = conn.backend().display_ptr() as *mut std::ffi::c_void; - let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; let event_loop = EventLoop::::try_new().unwrap(); let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); let handle = event_loop.handle(); - handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| { if let calloop::channel::Event::Msg(runnable) = event { runnable.run(); } }); - let globals = Globals::new(globals, common.foreground_executor.clone(), qh); + let seat = seat.unwrap(); + let globals = Globals::new(globals, common.foreground_executor.clone(), qh.clone()); + + let data_device = globals + .data_device_manager + .as_ref() + .map(|data_device_manager| data_device_manager.get_data_device(&seat, &qh, ())); + + let (primary, clipboard) = unsafe { create_clipboards_from_external(display) }; let cursor = Cursor::new(&conn, &globals, 24); let mut state = Rc::new(RefCell::new(WaylandClientState { + serial: 0, globals, wl_pointer: None, + data_device, output_scales: outputs, windows: HashMap::default(), common, keymap_state: None, + drag: DragState { + data_offer: None, + window: None, + position: Point::default(), + }, click: ClickState { last_click: Instant::now(), - last_location: Point::new(px(0.0), px(0.0)), + last_location: Point::default(), current_count: 0, }, repeat: KeyRepeat { @@ -467,6 +516,7 @@ impl Dispatch for WaylandClientStat } delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); +delegate_noop!(WaylandClientStatePtr: ignore wl_data_device_manager::WlDataDeviceManager); delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm); delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool); delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); @@ -599,7 +649,7 @@ impl Dispatch for WaylandClientStatePtr { impl Dispatch for WaylandClientStatePtr { fn event( - _: &mut Self, + this: &mut Self, wm_base: &xdg_wm_base::XdgWmBase, event: ::Event, _: &(), @@ -607,6 +657,9 @@ impl Dispatch for WaylandClientStatePtr { _: &QueueHandle, ) { if let xdg_wm_base::Event::Ping { serial } = event { + let client = this.get_client(); + let mut state = client.borrow_mut(); + state.serial = serial; wm_base.pong(serial); } } @@ -678,7 +731,10 @@ impl Dispatch for WaylandClientStatePtr { }; state.keymap_state = Some(xkb::State::new(&keymap)); } - wl_keyboard::Event::Enter { surface, .. } => { + wl_keyboard::Event::Enter { + serial, surface, .. + } => { + state.serial = serial; state.keyboard_focused_window = get_window(&mut state, &surface.id()); if let Some(window) = state.keyboard_focused_window.clone() { @@ -686,7 +742,10 @@ impl Dispatch for WaylandClientStatePtr { window.set_focused(true); } } - wl_keyboard::Event::Leave { surface, .. } => { + wl_keyboard::Event::Leave { + serial, surface, .. + } => { + state.serial = serial; let keyboard_focused_window = get_window(&mut state, &surface.id()); state.keyboard_focused_window = None; @@ -696,12 +755,14 @@ impl Dispatch for WaylandClientStatePtr { } } wl_keyboard::Event::Modifiers { + serial, mods_depressed, mods_latched, mods_locked, group, .. } => { + state.serial = serial; let focused_window = state.keyboard_focused_window.clone(); let Some(focused_window) = focused_window else { return; @@ -721,8 +782,11 @@ impl Dispatch for WaylandClientStatePtr { wl_keyboard::Event::Key { key, state: WEnum::Value(key_state), + serial, .. } => { + state.serial = serial; + let focused_window = state.keyboard_focused_window.clone(); let Some(focused_window) = focused_window else { return; @@ -833,6 +897,7 @@ impl Dispatch for WaylandClientStatePtr { surface_y, .. } => { + state.serial = serial; state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32))); if let Some(window) = get_window(&mut state, &surface.id()) { @@ -885,10 +950,12 @@ impl Dispatch for WaylandClientStatePtr { } } wl_pointer::Event::Button { + serial, button, state: WEnum::Value(button_state), .. } => { + state.serial = serial; let button = linux_button_to_gpui(button); let Some(button) = button else { return }; if state.mouse_focused_window.is_none() { @@ -1123,3 +1190,163 @@ impl Dispatch window.handle_toplevel_decoration_event(event); } } + +const FILE_LIST_MIME_TYPE: &str = "text/uri-list"; + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &wl_data_device::WlDataDevice, + event: wl_data_device::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_device::Event::Enter { + serial, + surface, + x, + y, + id: data_offer, + } => { + state.serial = serial; + if let Some(data_offer) = data_offer { + let Some(drag_window) = get_window(&mut state, &surface.id()) else { + return; + }; + + const ACTIONS: DndAction = DndAction::Copy; + data_offer.set_actions(ACTIONS, ACTIONS); + + let pipe = Pipe::new().unwrap(); + data_offer.receive(FILE_LIST_MIME_TYPE.to_string(), unsafe { + BorrowedFd::borrow_raw(pipe.write.as_raw_fd()) + }); + let fd = pipe.read; + drop(pipe.write); + + let read_task = state + .common + .background_executor + .spawn(async { unsafe { read_fd(fd) } }); + + let this = this.clone(); + state + .common + .foreground_executor + .spawn(async move { + let file_list = match read_task.await { + Ok(list) => list, + Err(err) => { + log::error!("error reading drag and drop pipe: {err:?}"); + return; + } + }; + + let paths: SmallVec<[_; 2]> = file_list + .lines() + .map(|path| PathBuf::from(path.replace("file://", ""))) + .collect(); + let position = Point::new(x.into(), y.into()); + + // Prevent dropping text from other programs. + if paths.is_empty() { + data_offer.finish(); + data_offer.destroy(); + return; + } + + let input = PlatformInput::FileDrop(FileDropEvent::Entered { + position, + paths: crate::ExternalPaths(paths), + }); + + let client = this.get_client(); + let mut state = client.borrow_mut(); + state.drag.data_offer = Some(data_offer); + state.drag.window = Some(drag_window.clone()); + state.drag.position = position; + + drop(state); + drag_window.handle_input(input); + }) + .detach(); + } + } + wl_data_device::Event::Motion { x, y, .. } => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let position = Point::new(x.into(), y.into()); + state.drag.position = position; + + let input = PlatformInput::FileDrop(FileDropEvent::Pending { position }); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Leave => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Exited {}); + drop(state); + drag_window.handle_input(input); + } + wl_data_device::Event::Drop => { + let Some(drag_window) = state.drag.window.clone() else { + return; + }; + let data_offer = state.drag.data_offer.clone().unwrap(); + data_offer.finish(); + data_offer.destroy(); + + state.drag.data_offer = None; + state.drag.window = None; + + let input = PlatformInput::FileDrop(FileDropEvent::Submit { + position: state.drag.position, + }); + drop(state); + drag_window.handle_input(input); + } + _ => {} + } + } + + event_created_child!(WaylandClientStatePtr, wl_data_device::WlDataDevice, [ + wl_data_device::EVT_DATA_OFFER_OPCODE => (wl_data_offer::WlDataOffer, ()), + ]); +} + +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + data_offer: &wl_data_offer::WlDataOffer, + event: wl_data_offer::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + + match event { + wl_data_offer::Event::Offer { mime_type } => { + if mime_type == FILE_LIST_MIME_TYPE { + data_offer.accept(state.serial, Some(mime_type)); + } + } + _ => {} + } + } +} From efcd31c254c6f1d304601b7310c5c1b1f5b770b2 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Tue, 23 Apr 2024 05:41:31 -0700 Subject: [PATCH 021/101] Update documentation and handling to use a `crates/collab/seed.json` (#10874) Updates `collab` to accept a `seed.json` file that allows you to override the defaults. Updated the `README` in collab to just have directions inside instead of redirecting the developer to the website. Release Notes: - N/A Co-authored-by: Max --- .gitignore | 2 +- crates/collab/README.md | 38 +++++++++++++++++++++++++++++++++++++- script/zed-local | 17 ++++++++++++++--- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 0b0f4d4ef8..1e5e9b0bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /plugins/bin /script/node_modules /crates/theme/schemas/theme.json -/crates/collab/.admins.json +/crates/collab/seed.json /assets/*licenses.md **/venv .build diff --git a/crates/collab/README.md b/crates/collab/README.md index 1af0b55d47..4e73f4b416 100644 --- a/crates/collab/README.md +++ b/crates/collab/README.md @@ -6,7 +6,43 @@ It contains our back-end logic for collaboration, to which we connect from the Z # Local Development -Detailed instructions on getting started are [here](https://zed.dev/docs/local-collaboration). +## Database setup + +Before you can run the collab server locally, you'll need to set up a zed Postgres database. + +``` +script/bootstrap +``` + +This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API. + +The script will create several _admin_ users, who you'll sign in as by default when developing locally. The GitHub logins for the default users are specified in the `seed.default.json` file. + +To use a different set of admin users, create `crates/collab/seed.json`. + +```json +{ + "admins": ["yourgithubhere"], + "channels": ["zed"], + "number_of_users": 20 +} +``` + +## Testing collaborative features locally + +In one terminal, run Zed's collaboration server and the livekit dev server: + +``` +foreman start +``` + +In a second terminal, run two or more instances of Zed. + +``` +script/zed-local -2 +``` + +This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `seed.json` or `seed.default.json`. # Deployment diff --git a/script/zed-local b/script/zed-local index b8d588c687..af15ec8755 100755 --- a/script/zed-local +++ b/script/zed-local @@ -20,9 +20,20 @@ OPTIONS const { spawn, execFileSync } = require("child_process"); const assert = require("assert"); -const users = require( - process.env.SEED_PATH || "../crates/collab/seed.default.json", -).admins; +let users; +if (process.env.SEED_PATH) { + users = require(process.env.SEED_PATH).admins; +} else { + users = require("../crates/collab/seed.default.json").admins; + try { + const defaultUsers = users; + const customUsers = require("../crates/collab/seed.json").admins; + assert(customUsers.length > 0); + users = customUsers.concat( + defaultUsers.filter((user) => !customUsers.includes(user)), + ); + } catch (_) {} +} const RESOLUTION_REGEX = /(\d+) x (\d+)/; const DIGIT_FLAG_REGEX = /^--?(\d+)$/; From bcbf2f2fd372fe8715385bdce8f272014f2f2510 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 23 Apr 2024 15:14:22 +0200 Subject: [PATCH 022/101] Introduce autoscroll support for elements (#10889) This pull request introduces the new `ElementContext::request_autoscroll(bounds)` and `ElementContext::take_autoscroll()` methods in GPUI. These new APIs enable container elements such as `List` to change their scroll position if one of their children requested an autoscroll. We plan to use this in the revamped assistant. As a drive-by, we also: - Renamed `Element::before_layout` to `Element::request_layout` - Renamed `Element::after_layout` to `Element::prepaint` - Introduced a new `List::splice_focusable` method to splice focusable elements into the list, which enables rendering offscreen elements that are focused. Release Notes: - N/A --------- Co-authored-by: Nathan --- crates/assistant/src/assistant_panel.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 106 ++++-- crates/editor/src/scroll.rs | 2 +- crates/editor/src/scroll/autoscroll.rs | 4 + crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/gpui/src/app/test_context.rs | 3 +- crates/gpui/src/element.rs | 208 +++++------ crates/gpui/src/elements/anchored.rs | 25 +- crates/gpui/src/elements/animation.rs | 22 +- crates/gpui/src/elements/canvas.rs | 27 +- crates/gpui/src/elements/deferred.rs | 16 +- crates/gpui/src/elements/div.rs | 76 ++-- crates/gpui/src/elements/img.rs | 18 +- crates/gpui/src/elements/list.rs | 343 ++++++++++++------ crates/gpui/src/elements/svg.rs | 16 +- crates/gpui/src/elements/text.rs | 54 +-- crates/gpui/src/elements/uniform_list.rs | 27 +- crates/gpui/src/key_dispatch.rs | 13 + crates/gpui/src/taffy.rs | 2 +- crates/gpui/src/text_system.rs | 4 + crates/gpui/src/text_system/line_layout.rs | 8 + crates/gpui/src/view.rs | 63 ++-- crates/gpui/src/window.rs | 7 + crates/gpui/src/window/element_cx.rs | 114 ++++-- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 22 +- crates/ui/src/components/popover_menu.rs | 33 +- crates/ui/src/components/right_click_menu.rs | 35 +- crates/workspace/src/pane_group.rs | 19 +- crates/workspace/src/workspace.rs | 18 +- 31 files changed, 780 insertions(+), 513 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 61c5c38d12..cfb8b983bb 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1108,7 +1108,7 @@ impl AssistantPanel { ) .track_scroll(scroll_handle) .into_any_element(); - saved_conversations.layout( + saved_conversations.prepaint_as_root( bounds.origin, bounds.size.map(AvailableSpace::Definite), cx, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6cd9680d0e..d7dc3caed7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10801,7 +10801,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren let icon_size = buttons(&diagnostic, cx.block_id) .into_any_element() - .measure(AvailableSpace::min_size(), cx); + .layout_as_root(AvailableSpace::min_size(), cx); h_flex() .id(cx.block_id) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 98837be7b9..b9fed082fc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -864,7 +864,7 @@ impl EditorElement { }), ) .into_any(); - hover_element.layout(fold_bounds.origin, fold_bounds.size.into(), cx); + hover_element.prepaint_as_root(fold_bounds.origin, fold_bounds.size.into(), cx); Some(FoldLayout { display_range, hover_element, @@ -882,12 +882,15 @@ impl EditorElement { line_layouts: &[LineWithInvisibles], text_hitbox: &Hitbox, content_origin: gpui::Point, + scroll_position: gpui::Point, scroll_pixel_position: gpui::Point, line_height: Pixels, em_width: Pixels, + autoscroll_containing_element: bool, cx: &mut ElementContext, ) -> Vec { - self.editor.update(cx, |editor, cx| { + let mut autoscroll_bounds = None; + let cursor_layouts = self.editor.update(cx, |editor, cx| { let mut cursors = Vec::new(); for (player_color, selections) in selections { for selection in selections { @@ -932,7 +935,7 @@ impl EditorElement { cursor_row_layout.font_size, &[TextRun { len, - font: font, + font, color: self.style.background, background_color: None, strikethrough: None, @@ -953,7 +956,27 @@ impl EditorElement { editor.pixel_position_of_newest_cursor = Some(point( text_hitbox.origin.x + x + block_width / 2., text_hitbox.origin.y + y + line_height / 2., - )) + )); + + if autoscroll_containing_element { + let top = text_hitbox.origin.y + + (cursor_position.row() as f32 - scroll_position.y - 3.).max(0.) + * line_height; + let left = text_hitbox.origin.x + + (cursor_position.column() as f32 - scroll_position.x - 3.) + .max(0.) + * em_width; + + let bottom = text_hitbox.origin.y + + (cursor_position.row() as f32 - scroll_position.y + 4.) + * line_height; + let right = text_hitbox.origin.x + + (cursor_position.column() as f32 - scroll_position.x + 4.) + * em_width; + + autoscroll_bounds = + Some(Bounds::from_corners(point(left, top), point(right, bottom))) + } } let mut cursor = CursorLayout { @@ -975,7 +998,13 @@ impl EditorElement { } } cursors - }) + }); + + if let Some(bounds) = autoscroll_bounds { + cx.request_autoscroll(bounds); + } + + cursor_layouts } fn layout_scrollbar( @@ -1073,7 +1102,7 @@ impl EditorElement { AvailableSpace::MinContent, AvailableSpace::Definite(line_height * 0.55), ); - let fold_indicator_size = fold_indicator.measure(available_space, cx); + let fold_indicator_size = fold_indicator.layout_as_root(available_space, cx); let position = point( gutter_dimensions.width - gutter_dimensions.right_padding, @@ -1086,7 +1115,7 @@ impl EditorElement { (line_height - fold_indicator_size.height) / 2., ); let origin = gutter_hitbox.origin + position + centering_offset; - fold_indicator.layout(origin, available_space, cx); + fold_indicator.prepaint_as_root(origin, available_space, cx); } } @@ -1177,7 +1206,7 @@ impl EditorElement { let absolute_offset = point(start_x, start_y); let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - element.layout(absolute_offset, available_space, cx); + element.prepaint_as_root(absolute_offset, available_space, cx); Some(element) } @@ -1233,7 +1262,11 @@ impl EditorElement { let start_y = ix as f32 * line_height - (scroll_top % line_height); let absolute_offset = gutter_hitbox.origin + point(start_x, start_y); - element.layout(absolute_offset, size(width, AvailableSpace::MinContent), cx); + element.prepaint_as_root( + absolute_offset, + size(width, AvailableSpace::MinContent), + cx, + ); Some(element) } else { @@ -1269,7 +1302,7 @@ impl EditorElement { AvailableSpace::MinContent, AvailableSpace::Definite(line_height), ); - let indicator_size = button.measure(available_space, cx); + let indicator_size = button.layout_as_root(available_space, cx); let blame_width = gutter_dimensions .git_blame_entries_width @@ -1284,7 +1317,7 @@ impl EditorElement { let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y; y += (line_height - indicator_size.height) / 2.; - button.layout(gutter_hitbox.origin + point(x, y), available_space, cx); + button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx); Some(button) } @@ -1773,7 +1806,7 @@ impl EditorElement { } }; - let size = element.measure(available_space, cx); + let size = element.layout_as_root(available_space, cx); (element, size) }; @@ -1843,7 +1876,9 @@ impl EditorElement { if !matches!(block.style, BlockStyle::Sticky) { origin += point(-scroll_pixel_position.x, Pixels::ZERO); } - block.element.layout(origin, block.available_space, cx); + block + .element + .prepaint_as_root(origin, block.available_space, cx); } } @@ -1875,7 +1910,7 @@ impl EditorElement { }; let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - let context_menu_size = context_menu.measure(available_space, cx); + let context_menu_size = context_menu.layout_as_root(available_space, cx); let cursor_row_layout = &line_layouts[(position.row() - start_row) as usize].line; let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x; @@ -1910,7 +1945,7 @@ impl EditorElement { .with_priority(1) .into_any(); - element.layout(gpui::Point::default(), AvailableSpace::min_size(), cx); + element.prepaint_as_root(gpui::Point::default(), AvailableSpace::min_size(), cx); Some(element) } @@ -1972,7 +2007,7 @@ impl EditorElement { let mut overall_height = Pixels::ZERO; let mut measured_hover_popovers = Vec::new(); for mut hover_popover in hover_popovers { - let size = hover_popover.measure(available_space, cx); + let size = hover_popover.layout_as_root(available_space, cx); let horizontal_offset = (text_hitbox.upper_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO); @@ -1992,7 +2027,7 @@ impl EditorElement { .occlude() .on_mouse_move(|_, cx| cx.stop_propagation()) .into_any_element(); - occlusion.measure(size(width, HOVER_POPOVER_GAP).into(), cx); + occlusion.layout_as_root(size(width, HOVER_POPOVER_GAP).into(), cx); cx.defer_draw(occlusion, origin, 2); } @@ -3327,10 +3362,10 @@ enum Invisible { } impl Element for EditorElement { - type BeforeLayout = (); - type AfterLayout = EditorLayout; + type RequestLayoutState = (); + type PrepaintState = EditorLayout; - fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, ()) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, ()) { self.editor.update(cx, |editor, cx| { editor.set_style(self.style.clone(), cx); @@ -3377,12 +3412,12 @@ impl Element for EditorElement { }) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, + _: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> Self::AfterLayout { + ) -> Self::PrepaintState { let text_style = TextStyleRefinement { font_size: Some(self.style.text.font_size), line_height: Some(self.style.text.line_height), @@ -3466,11 +3501,12 @@ impl Element for EditorElement { let content_origin = text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO); - let autoscroll_horizontally = self.editor.update(cx, |editor, cx| { - let autoscroll_horizontally = - editor.autoscroll_vertically(bounds, line_height, cx); + let mut autoscroll_requested = false; + let mut autoscroll_horizontally = false; + self.editor.update(cx, |editor, cx| { + autoscroll_requested = editor.autoscroll_requested(); + autoscroll_horizontally = editor.autoscroll_vertically(bounds, line_height, cx); snapshot = editor.snapshot(cx); - autoscroll_horizontally }); let mut scroll_position = snapshot.scroll_position(); @@ -3643,9 +3679,11 @@ impl Element for EditorElement { &line_layouts, &text_hitbox, content_origin, + scroll_position, scroll_pixel_position, line_height, em_width, + autoscroll_requested, cx, ); @@ -3806,8 +3844,8 @@ impl Element for EditorElement { fn paint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, - layout: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + layout: &mut Self::PrepaintState, cx: &mut ElementContext, ) { let focus_handle = self.editor.focus_handle(cx); @@ -4187,7 +4225,7 @@ impl CursorLayout { .child(cursor_name.string.clone()) .into_any_element(); - name_element.layout( + name_element.prepaint_as_root( name_origin, size(AvailableSpace::MinContent, AvailableSpace::MinContent), cx, @@ -4467,7 +4505,7 @@ mod tests { let state = cx .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - element.after_layout( + element.prepaint( Bounds { origin: point(px(500.), px(500.)), size: size(px(500.), px(500.)), @@ -4562,7 +4600,7 @@ mod tests { let state = cx .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - element.after_layout( + element.prepaint( Bounds { origin: point(px(500.), px(500.)), size: size(px(500.), px(500.)), @@ -4627,7 +4665,7 @@ mod tests { let state = cx .update_window(window.into(), |_view, cx| { cx.with_element_context(|cx| { - element.after_layout( + element.prepaint( Bounds { origin: point(px(500.), px(500.)), size: size(px(500.), px(500.)), @@ -4823,7 +4861,7 @@ mod tests { let layout_state = cx .update_window(window.into(), |_, cx| { cx.with_element_context(|cx| { - element.after_layout( + element.prepaint( Bounds { origin: point(px(500.), px(500.)), size: size(px(500.), px(500.)), diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index cb62d30d42..14f6edc1d4 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -275,7 +275,7 @@ impl ScrollManager { self.show_scrollbars } - pub fn has_autoscroll_request(&self) -> bool { + pub fn autoscroll_requested(&self) -> bool { self.autoscroll_request.is_some() } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index b5708649cc..ccf0126b1e 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -61,6 +61,10 @@ impl AutoscrollStrategy { } impl Editor { + pub fn autoscroll_requested(&self) -> bool { + self.scroll_manager.autoscroll_requested() + } + pub fn autoscroll_vertically( &mut self, bounds: Bounds, diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 25ef796784..fcc1fd8695 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -948,7 +948,7 @@ impl Render for ExtensionsPage { .pb_4() .track_scroll(scroll_handle) .into_any_element(); - list.layout(bounds.origin, bounds.size.into(), cx); + list.prepaint_as_root(bounds.origin, bounds.size.into(), cx); list }, |_bounds, mut list, cx| list.paint(cx), diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index d049ceef2f..7a6f3b9e27 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -734,7 +734,8 @@ impl VisualTestContext { self.update(|cx| { cx.with_element_context(|cx| { let mut element = f(cx); - element.layout(origin, space, cx); + element.layout_as_root(space, cx); + cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx)); element.paint(cx); }); diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index ccb4a6249d..ddb728e437 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -44,34 +44,34 @@ use std::{any::Any, fmt::Debug, mem, ops::DerefMut}; /// You can create custom elements by implementing this trait, see the module-level documentation /// for more details. pub trait Element: 'static + IntoElement { - /// The type of state returned from [`Element::before_layout`]. A mutable reference to this state is subsequently - /// provided to [`Element::after_layout`] and [`Element::paint`]. - type BeforeLayout: 'static; + /// The type of state returned from [`Element::request_layout`]. A mutable reference to this state is subsequently + /// provided to [`Element::prepaint`] and [`Element::paint`]. + type RequestLayoutState: 'static; - /// The type of state returned from [`Element::after_layout`]. A mutable reference to this state is subsequently + /// The type of state returned from [`Element::prepaint`]. A mutable reference to this state is subsequently /// provided to [`Element::paint`]. - type AfterLayout: 'static; + type PrepaintState: 'static; /// Before an element can be painted, we need to know where it's going to be and how big it is. /// Use this method to request a layout from Taffy and initialize the element's state. - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout); + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState); /// After laying out an element, we need to commit its bounds to the current frame for hitbox - /// purposes. The state argument is the same state that was returned from [`Element::before_layout()`]. - fn after_layout( + /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. + fn prepaint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> Self::AfterLayout; + ) -> Self::PrepaintState; /// Once layout has been completed, this method will be called to paint the element to the screen. - /// The state argument is the same state that was returned from [`Element::before_layout()`]. + /// The state argument is the same state that was returned from [`Element::request_layout()`]. fn paint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, - after_layout: &mut Self::AfterLayout, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, cx: &mut ElementContext, ); @@ -161,34 +161,29 @@ impl Component { } impl Element for Component { - type BeforeLayout = AnyElement; - type AfterLayout = (); + type RequestLayoutState = AnyElement; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut element = self .0 .take() .unwrap() .render(cx.deref_mut()) .into_any_element(); - let layout_id = element.before_layout(cx); + let layout_id = element.request_layout(cx); (layout_id, element) } - fn after_layout( - &mut self, - _: Bounds, - element: &mut AnyElement, - cx: &mut ElementContext, - ) { - element.after_layout(cx); + fn prepaint(&mut self, _: Bounds, element: &mut AnyElement, cx: &mut ElementContext) { + element.prepaint(cx); } fn paint( &mut self, _: Bounds, - element: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { element.paint(cx) @@ -210,13 +205,13 @@ pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>); trait ElementObject { fn inner_element(&mut self) -> &mut dyn Any; - fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId; + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId; - fn after_layout(&mut self, cx: &mut ElementContext); + fn prepaint(&mut self, cx: &mut ElementContext); fn paint(&mut self, cx: &mut ElementContext); - fn measure( + fn layout_as_root( &mut self, available_space: Size, cx: &mut ElementContext, @@ -227,27 +222,27 @@ trait ElementObject { pub struct Drawable { /// The drawn element. pub element: E, - phase: ElementDrawPhase, + phase: ElementDrawPhase, } #[derive(Default)] -enum ElementDrawPhase { +enum ElementDrawPhase { #[default] Start, - BeforeLayout { + RequestLayoutState { layout_id: LayoutId, - before_layout: BeforeLayout, + request_layout: RequestLayoutState, }, LayoutComputed { layout_id: LayoutId, available_space: Size, - before_layout: BeforeLayout, + request_layout: RequestLayoutState, }, - AfterLayout { + PrepaintState { node_id: DispatchNodeId, bounds: Bounds, - before_layout: BeforeLayout, - after_layout: AfterLayout, + request_layout: RequestLayoutState, + prepaint: PrepaintState, }, Painted, } @@ -261,91 +256,91 @@ impl Drawable { } } - fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { match mem::take(&mut self.phase) { ElementDrawPhase::Start => { - let (layout_id, before_layout) = self.element.before_layout(cx); - self.phase = ElementDrawPhase::BeforeLayout { + let (layout_id, request_layout) = self.element.request_layout(cx); + self.phase = ElementDrawPhase::RequestLayoutState { layout_id, - before_layout, + request_layout, }; layout_id } - _ => panic!("must call before_layout only once"), + _ => panic!("must call request_layout only once"), } } - fn after_layout(&mut self, cx: &mut ElementContext) { + fn prepaint(&mut self, cx: &mut ElementContext) { match mem::take(&mut self.phase) { - ElementDrawPhase::BeforeLayout { + ElementDrawPhase::RequestLayoutState { layout_id, - mut before_layout, + mut request_layout, } | ElementDrawPhase::LayoutComputed { layout_id, - mut before_layout, + mut request_layout, .. } => { let bounds = cx.layout_bounds(layout_id); let node_id = cx.window.next_frame.dispatch_tree.push_node(); - let after_layout = self.element.after_layout(bounds, &mut before_layout, cx); - self.phase = ElementDrawPhase::AfterLayout { + let prepaint = self.element.prepaint(bounds, &mut request_layout, cx); + self.phase = ElementDrawPhase::PrepaintState { node_id, bounds, - before_layout, - after_layout, + request_layout, + prepaint, }; cx.window.next_frame.dispatch_tree.pop_node(); } - _ => panic!("must call before_layout before after_layout"), + _ => panic!("must call request_layout before prepaint"), } } - fn paint(&mut self, cx: &mut ElementContext) -> E::BeforeLayout { + fn paint(&mut self, cx: &mut ElementContext) -> E::RequestLayoutState { match mem::take(&mut self.phase) { - ElementDrawPhase::AfterLayout { + ElementDrawPhase::PrepaintState { node_id, bounds, - mut before_layout, - mut after_layout, + mut request_layout, + mut prepaint, .. } => { cx.window.next_frame.dispatch_tree.set_active_node(node_id); self.element - .paint(bounds, &mut before_layout, &mut after_layout, cx); + .paint(bounds, &mut request_layout, &mut prepaint, cx); self.phase = ElementDrawPhase::Painted; - before_layout + request_layout } - _ => panic!("must call after_layout before paint"), + _ => panic!("must call prepaint before paint"), } } - fn measure( + fn layout_as_root( &mut self, available_space: Size, cx: &mut ElementContext, ) -> Size { if matches!(&self.phase, ElementDrawPhase::Start) { - self.before_layout(cx); + self.request_layout(cx); } let layout_id = match mem::take(&mut self.phase) { - ElementDrawPhase::BeforeLayout { + ElementDrawPhase::RequestLayoutState { layout_id, - before_layout, + request_layout, } => { cx.compute_layout(layout_id, available_space); self.phase = ElementDrawPhase::LayoutComputed { layout_id, available_space, - before_layout, + request_layout, }; layout_id } ElementDrawPhase::LayoutComputed { layout_id, available_space: prev_available_space, - before_layout, + request_layout, } => { if available_space != prev_available_space { cx.compute_layout(layout_id, available_space); @@ -353,7 +348,7 @@ impl Drawable { self.phase = ElementDrawPhase::LayoutComputed { layout_id, available_space, - before_layout, + request_layout, }; layout_id } @@ -367,30 +362,30 @@ impl Drawable { impl ElementObject for Drawable where E: Element, - E::BeforeLayout: 'static, + E::RequestLayoutState: 'static, { fn inner_element(&mut self) -> &mut dyn Any { &mut self.element } - fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { - Drawable::before_layout(self, cx) + fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + Drawable::request_layout(self, cx) } - fn after_layout(&mut self, cx: &mut ElementContext) { - Drawable::after_layout(self, cx); + fn prepaint(&mut self, cx: &mut ElementContext) { + Drawable::prepaint(self, cx); } fn paint(&mut self, cx: &mut ElementContext) { Drawable::paint(self, cx); } - fn measure( + fn layout_as_root( &mut self, available_space: Size, cx: &mut ElementContext, ) -> Size { - Drawable::measure(self, available_space, cx) + Drawable::layout_as_root(self, available_space, cx) } } @@ -401,7 +396,7 @@ impl AnyElement { pub(crate) fn new(element: E) -> Self where E: 'static + Element, - E::BeforeLayout: Any, + E::RequestLayoutState: Any, { let element = ELEMENT_ARENA .with_borrow_mut(|arena| arena.alloc(|| Drawable::new(element))) @@ -416,13 +411,14 @@ impl AnyElement { /// Request the layout ID of the element stored in this `AnyElement`. /// Used for laying out child elements in a parent element. - pub fn before_layout(&mut self, cx: &mut ElementContext) -> LayoutId { - self.0.before_layout(cx) + pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + self.0.request_layout(cx) } - /// Commits the element bounds of this [AnyElement] for hitbox purposes. - pub fn after_layout(&mut self, cx: &mut ElementContext) { - self.0.after_layout(cx) + /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and + /// request autoscroll before the final paint pass is confirmed. + pub fn prepaint(&mut self, cx: &mut ElementContext) { + self.0.prepaint(cx) } /// Paints the element stored in this `AnyElement`. @@ -430,51 +426,55 @@ impl AnyElement { self.0.paint(cx) } - /// Initializes this element and performs layout within the given available space to determine its size. - pub fn measure( + /// Performs layout for this element within the given available space and returns its size. + pub fn layout_as_root( &mut self, available_space: Size, cx: &mut ElementContext, ) -> Size { - self.0.measure(available_space, cx) + self.0.layout_as_root(available_space, cx) } - /// Initializes this element, performs layout if needed and commits its bounds for hitbox purposes. - pub fn layout( + /// Prepaints this element at the given absolute origin. + pub fn prepaint_at(&mut self, origin: Point, cx: &mut ElementContext) { + cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); + } + + /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. + pub fn prepaint_as_root( &mut self, - absolute_offset: Point, + origin: Point, available_space: Size, cx: &mut ElementContext, - ) -> Size { - let size = self.measure(available_space, cx); - cx.with_absolute_element_offset(absolute_offset, |cx| self.after_layout(cx)); - size + ) { + self.layout_as_root(available_space, cx); + cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); } } impl Element for AnyElement { - type BeforeLayout = (); - type AfterLayout = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { - let layout_id = self.before_layout(cx); + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.request_layout(cx); (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, _: Bounds, - _: &mut Self::BeforeLayout, + _: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) { - self.after_layout(cx) + self.prepaint(cx) } fn paint( &mut self, _: Bounds, - _: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { self.paint(cx) @@ -505,17 +505,17 @@ impl IntoElement for Empty { } impl Element for Empty { - type BeforeLayout = (); - type AfterLayout = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { (cx.request_layout(&crate::Style::default(), None), ()) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - _state: &mut Self::BeforeLayout, + _state: &mut Self::RequestLayoutState, _cx: &mut ElementContext, ) { } @@ -523,8 +523,8 @@ impl Element for Empty { fn paint( &mut self, _bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, - _after_layout: &mut Self::AfterLayout, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, _cx: &mut ElementContext, ) { } diff --git a/crates/gpui/src/elements/anchored.rs b/crates/gpui/src/elements/anchored.rs index 1915131277..9f4d342716 100644 --- a/crates/gpui/src/elements/anchored.rs +++ b/crates/gpui/src/elements/anchored.rs @@ -69,14 +69,17 @@ impl ParentElement for Anchored { } impl Element for Anchored { - type BeforeLayout = AnchoredState; - type AfterLayout = (); + type RequestLayoutState = AnchoredState; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) { + fn request_layout( + &mut self, + cx: &mut ElementContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { let child_layout_ids = self .children .iter_mut() - .map(|child| child.before_layout(cx)) + .map(|child| child.request_layout(cx)) .collect::>(); let anchored_style = Style { @@ -90,19 +93,19 @@ impl Element for Anchored { (layout_id, AnchoredState { child_layout_ids }) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) { - if before_layout.child_layout_ids.is_empty() { + if request_layout.child_layout_ids.is_empty() { return; } let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); - for child_layout_id in &before_layout.child_layout_ids { + for child_layout_id in &request_layout.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -167,7 +170,7 @@ impl Element for Anchored { cx.with_element_offset(offset, |cx| { for child in &mut self.children { - child.after_layout(cx); + child.prepaint(cx); } }) } @@ -175,8 +178,8 @@ impl Element for Anchored { fn paint( &mut self, _bounds: crate::Bounds, - _before_layout: &mut Self::BeforeLayout, - _after_layout: &mut Self::AfterLayout, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, cx: &mut ElementContext, ) { for child in &mut self.children { diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 4c3379cfb1..586b26f7e9 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -85,14 +85,14 @@ struct AnimationState { } impl Element for AnimationElement { - type BeforeLayout = AnyElement; + type RequestLayoutState = AnyElement; - type AfterLayout = (); + type PrepaintState = (); - fn before_layout( + fn request_layout( &mut self, cx: &mut crate::ElementContext, - ) -> (crate::LayoutId, Self::BeforeLayout) { + ) -> (crate::LayoutId, Self::RequestLayoutState) { cx.with_element_state(Some(self.id.clone()), |state, cx| { let state = state.unwrap().unwrap_or_else(|| AnimationState { start: Instant::now(), @@ -130,24 +130,24 @@ impl Element for AnimationElement { }) } - ((element.before_layout(cx), element), Some(state)) + ((element.request_layout(cx), element), Some(state)) }) } - fn after_layout( + fn prepaint( &mut self, _bounds: crate::Bounds, - element: &mut Self::BeforeLayout, + element: &mut Self::RequestLayoutState, cx: &mut crate::ElementContext, - ) -> Self::AfterLayout { - element.after_layout(cx); + ) -> Self::PrepaintState { + element.prepaint(cx); } fn paint( &mut self, _bounds: crate::Bounds, - element: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut crate::ElementContext, ) { element.paint(cx); diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index 623dfa2280..c0bfc044ab 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -5,11 +5,11 @@ use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRe /// Construct a canvas element with the given paint callback. /// Useful for adding short term custom drawing to a view. pub fn canvas( - after_layout: impl 'static + FnOnce(Bounds, &mut ElementContext) -> T, + prepaint: impl 'static + FnOnce(Bounds, &mut ElementContext) -> T, paint: impl 'static + FnOnce(Bounds, T, &mut ElementContext), ) -> Canvas { Canvas { - after_layout: Some(Box::new(after_layout)), + prepaint: Some(Box::new(prepaint)), paint: Some(Box::new(paint)), style: StyleRefinement::default(), } @@ -18,7 +18,7 @@ pub fn canvas( /// A canvas element, meant for accessing the low level paint API without defining a whole /// custom element pub struct Canvas { - after_layout: Option, &mut ElementContext) -> T>>, + prepaint: Option, &mut ElementContext) -> T>>, paint: Option, T, &mut ElementContext)>>, style: StyleRefinement, } @@ -32,35 +32,38 @@ impl IntoElement for Canvas { } impl Element for Canvas { - type BeforeLayout = Style; - type AfterLayout = Option; + type RequestLayoutState = Style; + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (crate::LayoutId, Self::BeforeLayout) { + fn request_layout( + &mut self, + cx: &mut ElementContext, + ) -> (crate::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.refine(&self.style); let layout_id = cx.request_layout(&style, []); (layout_id, style) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _before_layout: &mut Style, + _request_layout: &mut Style, cx: &mut ElementContext, ) -> Option { - Some(self.after_layout.take().unwrap()(bounds, cx)) + Some(self.prepaint.take().unwrap()(bounds, cx)) } fn paint( &mut self, bounds: Bounds, style: &mut Style, - after_layout: &mut Self::AfterLayout, + prepaint: &mut Self::PrepaintState, cx: &mut ElementContext, ) { - let after_layout = after_layout.take().unwrap(); + let prepaint = prepaint.take().unwrap(); style.paint(bounds, cx, |cx| { - (self.paint.take().unwrap())(bounds, after_layout, cx) + (self.paint.take().unwrap())(bounds, prepaint, cx) }); } } diff --git a/crates/gpui/src/elements/deferred.rs b/crates/gpui/src/elements/deferred.rs index 32a775f00a..30643bdc2a 100644 --- a/crates/gpui/src/elements/deferred.rs +++ b/crates/gpui/src/elements/deferred.rs @@ -26,18 +26,18 @@ impl Deferred { } impl Element for Deferred { - type BeforeLayout = (); - type AfterLayout = (); + type RequestLayoutState = (); + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) { - let layout_id = self.child.as_mut().unwrap().before_layout(cx); + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) { + let layout_id = self.child.as_mut().unwrap().request_layout(cx); (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, + _request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) { let child = self.child.take().unwrap(); @@ -48,8 +48,8 @@ impl Element for Deferred { fn paint( &mut self, _bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, - _after_layout: &mut Self::AfterLayout, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, _cx: &mut ElementContext, ) { } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 708aea464a..92c3420206 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1120,17 +1120,17 @@ impl ParentElement for Div { } impl Element for Div { - type BeforeLayout = DivFrameState; - type AfterLayout = Option; + type RequestLayoutState = DivFrameState; + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut child_layout_ids = SmallVec::new(); - let layout_id = self.interactivity.before_layout(cx, |style, cx| { + let layout_id = self.interactivity.request_layout(cx, |style, cx| { cx.with_text_style(style.text_style().cloned(), |cx| { child_layout_ids = self .children .iter_mut() - .map(|child| child.before_layout(cx)) + .map(|child| child.request_layout(cx)) .collect::>(); cx.request_layout(&style, child_layout_ids.iter().copied()) }) @@ -1138,23 +1138,23 @@ impl Element for Div { (layout_id, DivFrameState { child_layout_ids }) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); - let content_size = if before_layout.child_layout_ids.is_empty() { + let content_size = if request_layout.child_layout_ids.is_empty() { bounds.size } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { let mut state = scroll_handle.0.borrow_mut(); - state.child_bounds = Vec::with_capacity(before_layout.child_layout_ids.len()); + state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); state.bounds = bounds; let requested = state.requested_scroll_top.take(); - for (ix, child_layout_id) in before_layout.child_layout_ids.iter().enumerate() { + for (ix, child_layout_id) in request_layout.child_layout_ids.iter().enumerate() { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -1169,7 +1169,7 @@ impl Element for Div { } (child_max - child_min).into() } else { - for child_layout_id in &before_layout.child_layout_ids { + for child_layout_id in &request_layout.child_layout_ids { let child_bounds = cx.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); child_max = child_max.max(&child_bounds.lower_right()); @@ -1177,14 +1177,14 @@ impl Element for Div { (child_max - child_min).into() }; - self.interactivity.after_layout( + self.interactivity.prepaint( bounds, content_size, cx, |_style, scroll_offset, hitbox, cx| { cx.with_element_offset(scroll_offset, |cx| { for child in &mut self.children { - child.after_layout(cx); + child.prepaint(cx); } }); hitbox @@ -1195,7 +1195,7 @@ impl Element for Div { fn paint( &mut self, bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, + _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, cx: &mut ElementContext, ) { @@ -1274,7 +1274,7 @@ pub struct Interactivity { impl Interactivity { /// Layout this element according to this interactivity state's configured styles - pub fn before_layout( + pub fn request_layout( &mut self, cx: &mut ElementContext, f: impl FnOnce(Style, &mut ElementContext) -> LayoutId, @@ -1337,7 +1337,7 @@ impl Interactivity { } /// Commit the bounds of this element according to this interactivity state's configured styles. - pub fn after_layout( + pub fn prepaint( &mut self, bounds: Bounds, content_size: Size, @@ -2261,30 +2261,30 @@ impl Element for Focusable where E: Element, { - type BeforeLayout = E::BeforeLayout; - type AfterLayout = E::AfterLayout; + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { - self.element.before_layout(cx) + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(cx) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - state: &mut Self::BeforeLayout, + state: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> E::AfterLayout { - self.element.after_layout(bounds, state, cx) + ) -> E::PrepaintState { + self.element.prepaint(bounds, state, cx) } fn paint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, - after_layout: &mut Self::AfterLayout, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, cx: &mut ElementContext, ) { - self.element.paint(bounds, before_layout, after_layout, cx) + self.element.paint(bounds, request_layout, prepaint, cx) } } @@ -2344,30 +2344,30 @@ impl Element for Stateful where E: Element, { - type BeforeLayout = E::BeforeLayout; - type AfterLayout = E::AfterLayout; + type RequestLayoutState = E::RequestLayoutState; + type PrepaintState = E::PrepaintState; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { - self.element.before_layout(cx) + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + self.element.request_layout(cx) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - state: &mut Self::BeforeLayout, + state: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> E::AfterLayout { - self.element.after_layout(bounds, state, cx) + ) -> E::PrepaintState { + self.element.prepaint(bounds, state, cx) } fn paint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, - after_layout: &mut Self::AfterLayout, + request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, cx: &mut ElementContext, ) { - self.element.paint(bounds, before_layout, after_layout, cx); + self.element.paint(bounds, request_layout, prepaint, cx); } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 67c7c0a4ee..fad4a49fee 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -229,11 +229,11 @@ impl Img { } impl Element for Img { - type BeforeLayout = (); - type AfterLayout = Option; + type RequestLayoutState = (); + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { - let layout_id = self.interactivity.before_layout(cx, |mut style, cx| { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + let layout_id = self.interactivity.request_layout(cx, |mut style, cx| { if let Some(data) = self.source.data(cx) { let image_size = data.size(); match (style.size.width, style.size.height) { @@ -256,21 +256,21 @@ impl Element for Img { (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, + _request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { self.interactivity - .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) + .prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) } fn paint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, - hitbox: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, cx: &mut ElementContext, ) { let source = self.source.clone(); diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index b353da5e63..784955a5d7 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,8 +8,8 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, - Element, ElementContext, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, - StyleRefinement, Styled, WindowContext, + Element, ElementContext, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, + Size, Style, StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; @@ -92,20 +92,58 @@ pub enum ListSizingBehavior { struct LayoutItemsResponse { max_item_width: Pixels, scroll_top: ListOffset, - available_item_space: Size, - item_elements: VecDeque, + item_layouts: VecDeque, +} + +struct ItemLayout { + index: usize, + element: AnyElement, + size: Size, } /// Frame state used by the [List] element after layout. -pub struct ListAfterLayoutState { +pub struct ListPrepaintState { hitbox: Hitbox, layout: LayoutItemsResponse, } #[derive(Clone)] enum ListItem { - Unrendered, - Rendered { size: Size }, + Unmeasured { + focus_handle: Option, + }, + Measured { + size: Size, + focus_handle: Option, + }, +} + +impl ListItem { + fn size(&self) -> Option> { + if let ListItem::Measured { size, .. } = self { + Some(*size) + } else { + None + } + } + + fn focus_handle(&self) -> Option { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle.clone() + } + } + } + + fn contains_focused(&self, cx: &WindowContext) -> bool { + match self { + ListItem::Unmeasured { focus_handle } | ListItem::Measured { focus_handle, .. } => { + focus_handle + .as_ref() + .is_some_and(|handle| handle.contains_focused(cx)) + } + } + } } #[derive(Clone, Debug, Default, PartialEq)] @@ -114,6 +152,7 @@ struct ListItemSummary { rendered_count: usize, unrendered_count: usize, height: Pixels, + has_focus_handles: bool, } #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] @@ -131,45 +170,45 @@ struct Height(Pixels); impl ListState { /// Construct a new list state, for storage on a view. /// - /// the overdraw parameter controls how much extra space is rendered - /// above and below the visible area. This can help ensure that the list - /// doesn't flicker or pop in when scrolling. - pub fn new( - element_count: usize, - orientation: ListAlignment, + /// The overdraw parameter controls how much extra space is rendered + /// above and below the visible area. Elements within this area will + /// be measured even though they are not visible. This can help ensure + /// that the list doesn't flicker or pop in when scrolling. + pub fn new( + item_count: usize, + alignment: ListAlignment, overdraw: Pixels, - render_item: F, + render_item: R, ) -> Self where - F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement, + R: 'static + FnMut(usize, &mut WindowContext) -> AnyElement, { - let mut items = SumTree::new(); - items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); - Self(Rc::new(RefCell::new(StateInner { + let this = Self(Rc::new(RefCell::new(StateInner { last_layout_bounds: None, last_padding: None, render_item: Box::new(render_item), - items, + items: SumTree::new(), logical_scroll_top: None, - alignment: orientation, + alignment, overdraw, scroll_handler: None, reset: false, - }))) + }))); + this.splice(0..0, item_count); + this } /// Reset this instantiation of the list state. /// /// Note that this will cause scroll events to be dropped until the next paint. pub fn reset(&self, element_count: usize) { - let state = &mut *self.0.borrow_mut(); - state.reset = true; + { + let state = &mut *self.0.borrow_mut(); + state.reset = true; + state.logical_scroll_top = None; + } - state.logical_scroll_top = None; - state.items = SumTree::new(); - state - .items - .extend((0..element_count).map(|_| ListItem::Unrendered), &()); + self.splice(0..element_count, element_count); } /// The number of items in this list. @@ -177,11 +216,39 @@ impl ListState { self.0.borrow().items.summary().count } - /// Register with the list state that the items in `old_range` have been replaced + /// Inform the list state that the items in `old_range` have been replaced /// by `count` new items that must be recalculated. pub fn splice(&self, old_range: Range, count: usize) { + self.splice_focusable(old_range, (0..count).map(|_| None)) + } + + /// Register with the list state that the items in `old_range` have been replaced + /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles + /// to be supplied to properly integrate with items in the list that can be focused. If a focused item + /// is scrolled out of view, the list will continue to render it to allow keyboard interaction. + pub fn splice_focusable( + &self, + old_range: Range, + focus_handles: impl IntoIterator>, + ) { let state = &mut *self.0.borrow_mut(); + let mut old_items = state.items.cursor::(); + let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &()); + old_items.seek_forward(&Count(old_range.end), Bias::Right, &()); + + let mut spliced_count = 0; + new_items.extend( + focus_handles.into_iter().map(|focus_handle| { + spliced_count += 1; + ListItem::Unmeasured { focus_handle } + }), + &(), + ); + new_items.append(old_items.suffix(&()), &()); + drop(old_items); + state.items = new_items; + if let Some(ListOffset { item_ix, offset_in_item, @@ -191,18 +258,9 @@ impl ListState { *item_ix = old_range.start; *offset_in_item = px(0.); } else if old_range.end <= *item_ix { - *item_ix = *item_ix - (old_range.end - old_range.start) + count; + *item_ix = *item_ix - (old_range.end - old_range.start) + spliced_count; } } - - let mut old_heights = state.items.cursor::(); - let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &()); - old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); - - new_heights.extend((0..count).map(|_| ListItem::Unrendered), &()); - new_heights.append(old_heights.suffix(&()), &()); - drop(old_heights); - state.items = new_heights; } /// Set a handler that will be called when the list is scrolled. @@ -279,7 +337,7 @@ impl ListState { let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item; cursor.seek_forward(&Count(ix), Bias::Right, &()); - if let Some(&ListItem::Rendered { size }) = cursor.item() { + if let Some(&ListItem::Measured { size, .. }) = cursor.item() { let &(Count(count), Height(top)) = cursor.start(); if count == ix { let top = bounds.top() + top - scroll_top; @@ -379,10 +437,11 @@ impl StateInner { ) -> LayoutItemsResponse { let old_items = self.items.clone(); let mut measured_items = VecDeque::new(); - let mut item_elements = VecDeque::new(); + let mut item_layouts = VecDeque::new(); let mut rendered_height = padding.top; let mut max_item_width = px(0.); let mut scroll_top = self.logical_scroll_top(); + let mut rendered_focused_item = false; let available_item_space = size( available_width.map_or(AvailableSpace::MinContent, |width| { @@ -401,27 +460,34 @@ impl StateInner { break; } - // Use the previously cached height if available - let mut size = if let ListItem::Rendered { size } = item { - Some(*size) - } else { - None - }; + // Use the previously cached height and focus handle if available + let mut size = item.size(); // If we're within the visible area or the height wasn't cached, render and measure the item's element if visible_height < available_height || size.is_none() { - let mut element = (self.render_item)(scroll_top.item_ix + ix, cx); - let element_size = element.measure(available_item_space, cx); + let item_index = scroll_top.item_ix + ix; + let mut element = (self.render_item)(item_index, cx); + let element_size = element.layout_as_root(available_item_space, cx); size = Some(element_size); if visible_height < available_height { - item_elements.push_back(element); + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(cx) { + rendered_focused_item = true; + } } } let size = size.unwrap(); rendered_height += size.height; max_item_width = max_item_width.max(size.width); - measured_items.push_back(ListItem::Rendered { size }); + measured_items.push_back(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); } rendered_height += padding.bottom; @@ -433,13 +499,24 @@ impl StateInner { if rendered_height - scroll_top.offset_in_item < available_height { while rendered_height < available_height { cursor.prev(&()); - if cursor.item().is_some() { - let mut element = (self.render_item)(cursor.start().0, cx); - let element_size = element.measure(available_item_space, cx); - + if let Some(item) = cursor.item() { + let item_index = cursor.start().0; + let mut element = (self.render_item)(item_index, cx); + let element_size = element.layout_as_root(available_item_space, cx); + let focus_handle = item.focus_handle(); rendered_height += element_size.height; - measured_items.push_front(ListItem::Rendered { size: element_size }); - item_elements.push_front(element) + measured_items.push_front(ListItem::Measured { + size: element_size, + focus_handle, + }); + item_layouts.push_front(ItemLayout { + index: item_index, + element, + size: element_size, + }); + if item.contains_focused(cx) { + rendered_focused_item = true; + } } else { break; } @@ -470,15 +547,18 @@ impl StateInner { while leading_overdraw < self.overdraw { cursor.prev(&()); if let Some(item) = cursor.item() { - let size = if let ListItem::Rendered { size } = item { + let size = if let ListItem::Measured { size, .. } = item { *size } else { let mut element = (self.render_item)(cursor.start().0, cx); - element.measure(available_item_space, cx) + element.layout_as_root(available_item_space, cx) }; leading_overdraw += size.height; - measured_items.push_front(ListItem::Rendered { size }); + measured_items.push_front(ListItem::Measured { + size, + focus_handle: item.focus_handle(), + }); } else { break; } @@ -490,23 +570,83 @@ impl StateInner { new_items.extend(measured_items, &()); cursor.seek(&Count(measured_range.end), Bias::Right, &()); new_items.append(cursor.suffix(&()), &()); - self.items = new_items; + // If none of the visible items are focused, check if an off-screen item is focused + // and include it to be rendered after the visible items so keyboard interaction continues + // to work for it. + if !rendered_focused_item { + let mut cursor = self + .items + .filter::<_, Count>(|summary| summary.has_focus_handles); + cursor.next(&()); + while let Some(item) = cursor.item() { + if item.contains_focused(cx) { + let item_index = cursor.start().0; + let mut element = (self.render_item)(cursor.start().0, cx); + let size = element.layout_as_root(available_item_space, cx); + item_layouts.push_back(ItemLayout { + index: item_index, + element, + size, + }); + break; + } + cursor.next(&()); + } + } + LayoutItemsResponse { max_item_width, scroll_top, - available_item_space, - item_elements, + item_layouts, } } + + fn prepaint_items( + &mut self, + bounds: Bounds, + padding: Edges, + cx: &mut ElementContext, + ) -> Result { + cx.transact(|cx| { + let mut layout_response = + self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx); + + // Only paint the visible items, if there is actually any space for them (taking padding into account) + if bounds.size.height > padding.top + padding.bottom { + let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); + item_origin.y -= layout_response.scroll_top.offset_in_item; + for item in &mut layout_response.item_layouts { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + item.element.prepaint_at(item_origin, cx); + }); + + if let Some(autoscroll_bounds) = cx.take_autoscroll() { + if bounds.intersect(&autoscroll_bounds) != autoscroll_bounds { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.origin.y - item_origin.y, + }); + } + } + + item_origin.y += item.size.height; + } + } else { + layout_response.item_layouts.clear(); + } + + Ok(layout_response) + }) + } } impl std::fmt::Debug for ListItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Unrendered => write!(f, "Unrendered"), - Self::Rendered { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), + Self::Unmeasured { .. } => write!(f, "Unrendered"), + Self::Measured { size, .. } => f.debug_struct("Rendered").field("size", size).finish(), } } } @@ -522,13 +662,13 @@ pub struct ListOffset { } impl Element for List { - type BeforeLayout = (); - type AfterLayout = ListAfterLayoutState; + type RequestLayoutState = (); + type PrepaintState = ListPrepaintState; - fn before_layout( + fn request_layout( &mut self, cx: &mut crate::ElementContext, - ) -> (crate::LayoutId, Self::BeforeLayout) { + ) -> (crate::LayoutId, Self::RequestLayoutState) { let layout_id = match self.sizing_behavior { ListSizingBehavior::Infer => { let mut style = Style::default(); @@ -589,12 +729,12 @@ impl Element for List { (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, + _: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> ListAfterLayoutState { + ) -> ListPrepaintState { let state = &mut *self.state.0.borrow_mut(); state.reset = false; @@ -607,55 +747,47 @@ impl Element for List { if state.last_layout_bounds.map_or(true, |last_bounds| { last_bounds.size.width != bounds.size.width }) { - state.items = SumTree::from_iter( - (0..state.items.summary().count).map(|_| ListItem::Unrendered), + let new_items = SumTree::from_iter( + state.items.iter().map(|item| ListItem::Unmeasured { + focus_handle: item.focus_handle(), + }), &(), - ) + ); + + state.items = new_items; } let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - let mut layout_response = - state.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx); - - // Only paint the visible items, if there is actually any space for them (taking padding into account) - if bounds.size.height > padding.top + padding.bottom { - // Paint the visible items - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); - item_origin.y -= layout_response.scroll_top.offset_in_item; - for mut item_element in &mut layout_response.item_elements { - let item_size = item_element.measure(layout_response.available_item_space, cx); - item_element.layout(item_origin, layout_response.available_item_space, cx); - item_origin.y += item_size.height; - } - }); - } + let layout = match state.prepaint_items(bounds, padding, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state.prepaint_items(bounds, padding, cx).unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); - ListAfterLayoutState { - hitbox, - layout: layout_response, - } + ListPrepaintState { hitbox, layout } } fn paint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, - after_layout: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, cx: &mut crate::ElementContext, ) { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - for item in &mut after_layout.layout.item_elements { - item.paint(cx); + for item in &mut prepaint.layout.item_layouts { + item.element.paint(cx); } }); let list_state = self.state.clone(); let height = bounds.size.height; - let scroll_top = after_layout.layout.scroll_top; - let hitbox_id = after_layout.hitbox.id; + let scroll_top = prepaint.layout.scroll_top; + let hitbox_id = prepaint.hitbox.id; cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && hitbox_id.is_hovered(cx) { list_state.0.borrow_mut().scroll( @@ -688,17 +820,21 @@ impl sum_tree::Item for ListItem { fn summary(&self) -> Self::Summary { match self { - ListItem::Unrendered => ListItemSummary { + ListItem::Unmeasured { focus_handle } => ListItemSummary { count: 1, rendered_count: 0, unrendered_count: 1, height: px(0.), + has_focus_handles: focus_handle.is_some(), }, - ListItem::Rendered { size } => ListItemSummary { + ListItem::Measured { + size, focus_handle, .. + } => ListItemSummary { count: 1, rendered_count: 1, unrendered_count: 0, height: size.height, + has_focus_handles: focus_handle.is_some(), }, } } @@ -712,6 +848,7 @@ impl sum_tree::Summary for ListItemSummary { self.rendered_count += summary.rendered_count; self.unrendered_count += summary.unrendered_count; self.height += summary.height; + self.has_focus_handles |= summary.has_focus_handles; } } diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index d00c47e317..ae2f4c2074 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -37,30 +37,30 @@ impl Svg { } impl Element for Svg { - type BeforeLayout = (); - type AfterLayout = Option; + type RequestLayoutState = (); + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let layout_id = self .interactivity - .before_layout(cx, |style, cx| cx.request_layout(&style, None)); + .request_layout(cx, |style, cx| cx.request_layout(&style, None)); (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, + _request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { self.interactivity - .after_layout(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) + .prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) } fn paint( &mut self, bounds: Bounds, - _before_layout: &mut Self::BeforeLayout, + _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, cx: &mut ElementContext, ) where diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 4645404c29..565638bfb4 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -17,19 +17,19 @@ use std::{ use util::ResultExt; impl Element for &'static str { - type BeforeLayout = TextState; - type AfterLayout = (); + type RequestLayoutState = TextState; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - _text_state: &mut Self::BeforeLayout, + _text_state: &mut Self::RequestLayoutState, _cx: &mut ElementContext, ) { } @@ -62,19 +62,19 @@ impl IntoElement for String { } impl Element for SharedString { - type BeforeLayout = TextState; - type AfterLayout = (); + type RequestLayoutState = TextState; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - _text_state: &mut Self::BeforeLayout, + _text_state: &mut Self::RequestLayoutState, _cx: &mut ElementContext, ) { } @@ -82,8 +82,8 @@ impl Element for SharedString { fn paint( &mut self, bounds: Bounds, - text_state: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + text_state: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { let text_str: &str = self.as_ref(); @@ -148,19 +148,19 @@ impl StyledText { } impl Element for StyledText { - type BeforeLayout = TextState; - type AfterLayout = (); + type RequestLayoutState = TextState; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); (layout_id, state) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - _state: &mut Self::BeforeLayout, + _state: &mut Self::RequestLayoutState, _cx: &mut ElementContext, ) { } @@ -168,8 +168,8 @@ impl Element for StyledText { fn paint( &mut self, bounds: Bounds, - text_state: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + text_state: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { text_state.paint(bounds, &self.text, cx) @@ -402,17 +402,17 @@ impl InteractiveText { } impl Element for InteractiveText { - type BeforeLayout = TextState; - type AfterLayout = Hitbox; + type RequestLayoutState = TextState; + type PrepaintState = Hitbox; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { - self.text.before_layout(cx) + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + self.text.request_layout(cx) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - state: &mut Self::BeforeLayout, + state: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Hitbox { cx.with_element_state::( @@ -430,7 +430,7 @@ impl Element for InteractiveText { } } - self.text.after_layout(bounds, state, cx); + self.text.prepaint(bounds, state, cx); let hitbox = cx.insert_hitbox(bounds, false); (hitbox, interactive_state) }, @@ -440,7 +440,7 @@ impl Element for InteractiveText { fn paint( &mut self, bounds: Bounds, - text_state: &mut Self::BeforeLayout, + text_state: &mut Self::RequestLayoutState, hitbox: &mut Hitbox, cx: &mut ElementContext, ) { diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 82a9a48c01..2813043c7a 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -104,13 +104,13 @@ impl Styled for UniformList { } impl Element for UniformList { - type BeforeLayout = UniformListFrameState; - type AfterLayout = Option; + type RequestLayoutState = UniformListFrameState; + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let max_items = self.item_count; let item_size = self.measure_item(None, cx); - let layout_id = self.interactivity.before_layout(cx, |style, cx| { + let layout_id = self.interactivity.request_layout(cx, |style, cx| { cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| { let desired_height = item_size.height * max_items; let width = known_dimensions @@ -137,10 +137,10 @@ impl Element for UniformList { ) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + frame_state: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { let style = self.interactivity.compute_style(None, cx); @@ -155,7 +155,7 @@ impl Element for UniformList { let content_size = Size { width: padded_bounds.size.width, - height: before_layout.item_size.height * self.item_count + padding.top + padding.bottom, + height: frame_state.item_size.height * self.item_count + padding.top + padding.bottom, }; let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); @@ -166,7 +166,7 @@ impl Element for UniformList { .as_mut() .and_then(|handle| handle.deferred_scroll_to_item.take()); - self.interactivity.after_layout( + self.interactivity.prepaint( bounds, content_size, cx, @@ -222,8 +222,9 @@ impl Element for UniformList { AvailableSpace::Definite(padded_bounds.size.width), AvailableSpace::Definite(item_height), ); - item.layout(item_origin, available_space, cx); - before_layout.items.push(item); + item.layout_as_root(available_space, cx); + item.prepaint_at(item_origin, cx); + frame_state.items.push(item); } }); } @@ -236,13 +237,13 @@ impl Element for UniformList { fn paint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, cx: &mut ElementContext, ) { self.interactivity .paint(bounds, hitbox.as_ref(), cx, |_, cx| { - for item in &mut before_layout.items { + for item in &mut request_layout.items { item.paint(cx); } }) @@ -278,7 +279,7 @@ impl UniformList { }), AvailableSpace::MinContent, ); - item_to_measure.measure(available_space, cx) + item_to_measure.layout_as_root(available_space, cx) } /// Track and render scroll state of this list with reference to the given scroll handle. diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 2aa6931fab..ff0b995693 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -283,6 +283,19 @@ impl DispatchTree { } } + pub fn truncate(&mut self, index: usize) { + for node in &self.nodes[index..] { + if let Some(focus_id) = node.focus_id { + self.focusable_node_ids.remove(&focus_id); + } + + if let Some(view_id) = node.view_id { + self.view_node_ids.remove(&view_id); + } + } + self.nodes.truncate(index); + } + pub fn clear_pending_keystrokes(&mut self) { self.keystroke_matchers.clear(); } diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 248948d071..d5abd9add4 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -47,7 +47,7 @@ impl TaffyLayoutEngine { self.styles.clear(); } - pub fn before_layout( + pub fn request_layout( &mut self, style: &Style, rem_size: Pixels, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 4d044a2837..a03031600a 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -311,6 +311,10 @@ impl WindowTextSystem { self.line_layout_cache.reuse_layouts(index) } + pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) { + self.line_layout_cache.truncate_layouts(index) + } + /// Shape the given line, at the given font_size, for painting to the screen. /// Subsets of the line can be styled independently with the `runs` parameter. /// diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index c98e304c5f..067dbca17d 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -347,6 +347,14 @@ impl LineLayoutCache { } } + pub fn truncate_layouts(&self, index: LineLayoutIndex) { + let mut current_frame = &mut *self.current_frame.write(); + current_frame.used_lines.truncate(index.lines_index); + current_frame + .used_wrapped_lines + .truncate(index.wrapped_lines_index); + } + pub fn finish_frame(&self) { let mut prev_frame = self.previous_frame.lock(); let mut curr_frame = self.current_frame.write(); diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 2475f379f1..3d9fb82cd5 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,8 +1,8 @@ use crate::{ - seal::Sealed, AfterLayoutIndex, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, - ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle, - FocusableView, IntoElement, LayoutId, Model, PaintIndex, Pixels, Render, Style, - StyleRefinement, TextStyle, ViewContext, VisualContext, WeakModel, + seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, ContentMask, Element, + ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, + LayoutId, Model, PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, + TextStyle, ViewContext, VisualContext, WeakModel, }; use anyhow::{Context, Result}; use refineable::Refineable; @@ -23,7 +23,7 @@ pub struct View { impl Sealed for View {} struct AnyViewState { - after_layout_range: Range, + prepaint_range: Range, paint_range: Range, cache_key: ViewCacheKey, } @@ -90,34 +90,34 @@ impl View { } impl Element for View { - type BeforeLayout = AnyElement; - type AfterLayout = (); + type RequestLayoutState = AnyElement; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); - let layout_id = element.before_layout(cx); + let layout_id = element.request_layout(cx); (layout_id, element) }) } - fn after_layout( + fn prepaint( &mut self, _: Bounds, - element: &mut Self::BeforeLayout, + element: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) { cx.set_view_id(self.entity_id()); cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { - element.after_layout(cx) + element.prepaint(cx) }) } fn paint( &mut self, _: Bounds, - element: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + element: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { @@ -276,10 +276,10 @@ impl From> for AnyView { } impl Element for AnyView { - type BeforeLayout = Option; - type AfterLayout = Option; + type RequestLayoutState = Option; + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { if let Some(style) = self.cached_style.as_ref() { let mut root_style = Style::default(); root_style.refine(style); @@ -288,16 +288,16 @@ impl Element for AnyView { } else { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { let mut element = (self.render)(self, cx); - let layout_id = element.before_layout(cx); + let layout_id = element.request_layout(cx); (layout_id, Some(element)) }) } } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - element: &mut Self::BeforeLayout, + element: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { cx.set_view_id(self.entity_id()); @@ -317,23 +317,24 @@ impl Element for AnyView { && !cx.window.dirty_views.contains(&self.entity_id()) && !cx.window.refreshing { - let after_layout_start = cx.after_layout_index(); - cx.reuse_after_layout(element_state.after_layout_range.clone()); - let after_layout_end = cx.after_layout_index(); - element_state.after_layout_range = after_layout_start..after_layout_end; + let prepaint_start = cx.prepaint_index(); + cx.reuse_prepaint(element_state.prepaint_range.clone()); + let prepaint_end = cx.prepaint_index(); + element_state.prepaint_range = prepaint_start..prepaint_end; return (None, Some(element_state)); } } - let after_layout_start = cx.after_layout_index(); + let prepaint_start = cx.prepaint_index(); let mut element = (self.render)(self, cx); - element.layout(bounds.origin, bounds.size.into(), cx); - let after_layout_end = cx.after_layout_index(); + element.layout_as_root(bounds.size.into(), cx); + element.prepaint_at(bounds.origin, cx); + let prepaint_end = cx.prepaint_index(); ( Some(element), Some(AnyViewState { - after_layout_range: after_layout_start..after_layout_end, + prepaint_range: prepaint_start..prepaint_end, paint_range: PaintIndex::default()..PaintIndex::default(), cache_key: ViewCacheKey { bounds, @@ -347,7 +348,7 @@ impl Element for AnyView { } else { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { let mut element = element.take().unwrap(); - element.after_layout(cx); + element.prepaint(cx); Some(element) }) } @@ -356,8 +357,8 @@ impl Element for AnyView { fn paint( &mut self, _bounds: Bounds, - _: &mut Self::BeforeLayout, - element: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + element: &mut Self::PrepaintState, cx: &mut ElementContext, ) { if self.cached_style.is_some() { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index abddf3e3a7..3c6160515f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -284,6 +284,9 @@ pub struct Window { pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, pub(crate) text_style_stack: Vec, + pub(crate) element_offset_stack: Vec>, + pub(crate) content_mask_stack: Vec>, + pub(crate) requested_autoscroll: Option>, pub(crate) rendered_frame: Frame, pub(crate) next_frame: Frame, pub(crate) next_hitbox_id: HitboxId, @@ -549,6 +552,9 @@ impl Window { root_view: None, element_id_stack: GlobalElementId::default(), text_style_stack: Vec::new(), + element_offset_stack: Vec::new(), + content_mask_stack: Vec::new(), + requested_autoscroll: None, rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), next_frame_callbacks, @@ -1023,6 +1029,7 @@ impl<'a> WindowContext<'a> { #[profiling::function] pub fn draw(&mut self) { self.window.dirty.set(false); + self.window.requested_autoscroll = None; // Restore the previously-used input handler. if let Some(input_handler) = self.window.platform_window.take_input_handler() { diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 15758fb357..22dc083a19 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -121,7 +121,7 @@ pub(crate) struct DeferredDraw { text_style_stack: Vec, element: Option, absolute_offset: Point, - layout_range: Range, + prepaint_range: Range, paint_range: Range, } @@ -135,8 +135,6 @@ pub(crate) struct Frame { pub(crate) scene: Scene, pub(crate) hitboxes: Vec, pub(crate) deferred_draws: Vec, - pub(crate) content_mask_stack: Vec>, - pub(crate) element_offset_stack: Vec>, pub(crate) input_handlers: Vec>, pub(crate) tooltip_requests: Vec>, pub(crate) cursor_styles: Vec, @@ -145,7 +143,7 @@ pub(crate) struct Frame { } #[derive(Clone, Default)] -pub(crate) struct AfterLayoutIndex { +pub(crate) struct PrepaintStateIndex { hitboxes_index: usize, tooltips_index: usize, deferred_draws_index: usize, @@ -176,8 +174,6 @@ impl Frame { scene: Scene::default(), hitboxes: Vec::new(), deferred_draws: Vec::new(), - content_mask_stack: Vec::new(), - element_offset_stack: Vec::new(), input_handlers: Vec::new(), tooltip_requests: Vec::new(), cursor_styles: Vec::new(), @@ -399,29 +395,29 @@ impl<'a> ElementContext<'a> { // Layout all root elements. let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any(); - root_element.layout(Point::default(), self.window.viewport_size.into(), self); + root_element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); let mut sorted_deferred_draws = (0..self.window.next_frame.deferred_draws.len()).collect::>(); sorted_deferred_draws.sort_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority); - self.layout_deferred_draws(&sorted_deferred_draws); + self.prepaint_deferred_draws(&sorted_deferred_draws); let mut prompt_element = None; let mut active_drag_element = None; let mut tooltip_element = None; if let Some(prompt) = self.window.prompt.take() { let mut element = prompt.view.any_view().into_any(); - element.layout(Point::default(), self.window.viewport_size.into(), self); + element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); prompt_element = Some(element); self.window.prompt = Some(prompt); } else if let Some(active_drag) = self.app.active_drag.take() { let mut element = active_drag.view.clone().into_any(); let offset = self.mouse_position() - active_drag.cursor_offset; - element.layout(offset, AvailableSpace::min_size(), self); + element.prepaint_as_root(offset, AvailableSpace::min_size(), self); active_drag_element = Some(element); self.app.active_drag = Some(active_drag); } else { - tooltip_element = self.layout_tooltip(); + tooltip_element = self.prepaint_tooltip(); } self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position); @@ -441,12 +437,12 @@ impl<'a> ElementContext<'a> { } } - fn layout_tooltip(&mut self) -> Option { + fn prepaint_tooltip(&mut self) -> Option { let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?; let tooltip_request = tooltip_request.unwrap(); let mut element = tooltip_request.tooltip.view.clone().into_any(); let mouse_position = tooltip_request.tooltip.mouse_position; - let tooltip_size = element.measure(AvailableSpace::min_size(), self); + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); let window_bounds = Bounds { @@ -478,7 +474,7 @@ impl<'a> ElementContext<'a> { } } - self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx)); + self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); self.window.tooltip_bounds = Some(TooltipBounds { id: tooltip_request.id, @@ -487,7 +483,7 @@ impl<'a> ElementContext<'a> { Some(element) } - fn layout_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { assert_eq!(self.window.element_id_stack.len(), 0); let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); @@ -500,16 +496,16 @@ impl<'a> ElementContext<'a> { .dispatch_tree .set_active_node(deferred_draw.parent_node); - let layout_start = self.after_layout_index(); + let prepaint_start = self.prepaint_index(); if let Some(element) = deferred_draw.element.as_mut() { self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| { - element.after_layout(cx) + element.prepaint(cx) }); } else { - self.reuse_after_layout(deferred_draw.layout_range.clone()); + self.reuse_prepaint(deferred_draw.prepaint_range.clone()); } - let layout_end = self.after_layout_index(); - deferred_draw.layout_range = layout_start..layout_end; + let prepaint_end = self.prepaint_index(); + deferred_draw.prepaint_range = prepaint_start..prepaint_end; } assert_eq!( self.window.next_frame.deferred_draws.len(), @@ -546,8 +542,8 @@ impl<'a> ElementContext<'a> { self.window.element_id_stack.clear(); } - pub(crate) fn after_layout_index(&self) -> AfterLayoutIndex { - AfterLayoutIndex { + pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { + PrepaintStateIndex { hitboxes_index: self.window.next_frame.hitboxes.len(), tooltips_index: self.window.next_frame.tooltip_requests.len(), deferred_draws_index: self.window.next_frame.deferred_draws.len(), @@ -557,7 +553,7 @@ impl<'a> ElementContext<'a> { } } - pub(crate) fn reuse_after_layout(&mut self, range: Range) { + pub(crate) fn reuse_prepaint(&mut self, range: Range) { let window = &mut self.window; window.next_frame.hitboxes.extend( window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] @@ -595,7 +591,7 @@ impl<'a> ElementContext<'a> { priority: deferred_draw.priority, element: None, absolute_offset: deferred_draw.absolute_offset, - layout_range: deferred_draw.layout_range.clone(), + prepaint_range: deferred_draw.prepaint_range.clone(), paint_range: deferred_draw.paint_range.clone(), }), ); @@ -715,9 +711,9 @@ impl<'a> ElementContext<'a> { ) -> R { if let Some(mask) = mask { let mask = mask.intersect(&self.content_mask()); - self.window_mut().next_frame.content_mask_stack.push(mask); + self.window_mut().content_mask_stack.push(mask); let result = f(self); - self.window_mut().next_frame.content_mask_stack.pop(); + self.window_mut().content_mask_stack.pop(); result } else { f(self) @@ -746,15 +742,61 @@ impl<'a> ElementContext<'a> { offset: Point, f: impl FnOnce(&mut Self) -> R, ) -> R { - self.window_mut() - .next_frame - .element_offset_stack - .push(offset); + self.window_mut().element_offset_stack.push(offset); let result = f(self); - self.window_mut().next_frame.element_offset_stack.pop(); + self.window_mut().element_offset_stack.pop(); result } + /// Perform prepaint on child elements in a "retryable" manner, so that any side effects + /// of prepaints can be discarded before prepainting again. This is used to support autoscroll + /// where we need to prepaint children to detect the autoscroll bounds, then adjust the + /// element offset and prepaint again. See [`List`] for an example. + pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + let index = self.prepaint_index(); + let result = f(self); + if result.is_err() { + self.window + .next_frame + .hitboxes + .truncate(index.hitboxes_index); + self.window + .next_frame + .tooltip_requests + .truncate(index.tooltips_index); + self.window + .next_frame + .deferred_draws + .truncate(index.deferred_draws_index); + self.window + .next_frame + .dispatch_tree + .truncate(index.dispatch_tree_index); + self.window + .next_frame + .accessed_element_states + .truncate(index.accessed_element_states_index); + self.window + .text_system + .truncate_layouts(index.line_layout_index); + } + result + } + + /// When you call this method during [`prepaint`], containing elements will attempt to + /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call + /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element + /// that supports this method being called on the elements it contains. + pub fn request_autoscroll(&mut self, bounds: Bounds) { + self.window.requested_autoscroll = Some(bounds); + } + + /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior + /// described in [`request_autoscroll`]. + pub fn take_autoscroll(&mut self) -> Option> { + self.window.requested_autoscroll.take() + } + /// Remove an asset from GPUI's cache pub fn remove_cached_asset( &mut self, @@ -835,7 +877,6 @@ impl<'a> ElementContext<'a> { /// Obtain the current element offset. pub fn element_offset(&self) -> Point { self.window() - .next_frame .element_offset_stack .last() .copied() @@ -845,7 +886,6 @@ impl<'a> ElementContext<'a> { /// Obtain the current content mask. pub fn content_mask(&self) -> ContentMask { self.window() - .next_frame .content_mask_stack .last() .cloned() @@ -974,7 +1014,7 @@ impl<'a> ElementContext<'a> { assert_eq!( window.draw_phase, DrawPhase::Layout, - "defer_draw can only be called during before_layout or after_layout" + "defer_draw can only be called during request_layout or prepaint" ); let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap(); window.next_frame.deferred_draws.push(DeferredDraw { @@ -984,7 +1024,7 @@ impl<'a> ElementContext<'a> { priority, element: Some(element), absolute_offset, - layout_range: AfterLayoutIndex::default()..AfterLayoutIndex::default(), + prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), paint_range: PaintIndex::default()..PaintIndex::default(), }); } @@ -1349,7 +1389,7 @@ impl<'a> ElementContext<'a> { .layout_engine .as_mut() .unwrap() - .before_layout(style, rem_size, &self.cx.app.layout_id_buffer) + .request_layout(style, rem_size, &self.cx.app.layout_id_buffer) } /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, @@ -1397,7 +1437,7 @@ impl<'a> ElementContext<'a> { bounds } - /// This method should be called during `after_layout`. You can use + /// This method should be called during `prepaint`. You can use /// the returned [Hitbox] during `paint` or in an event handler /// to determine whether the inserted hitbox was the topmost. pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index a8a3906b53..f4cd38f2ca 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -365,7 +365,7 @@ impl Render for SyntaxTreeView { rendered = rendered.child( canvas( move |bounds, cx| { - list.layout(bounds.origin, bounds.size.into(), cx); + list.prepaint_as_root(bounds.origin, bounds.size.into(), cx); list }, |_, mut list, cx| list.paint(cx), diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 5f06d7f038..7547c9603f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -541,12 +541,12 @@ impl TerminalElement { } impl Element for TerminalElement { - type BeforeLayout = (); - type AfterLayout = LayoutState; + type RequestLayoutState = (); + type PrepaintState = LayoutState; - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { self.interactivity.occlude_mouse(); - let layout_id = self.interactivity.before_layout(cx, |mut style, cx| { + let layout_id = self.interactivity.request_layout(cx, |mut style, cx| { style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); let layout_id = cx.request_layout(&style, None); @@ -556,14 +556,14 @@ impl Element for TerminalElement { (layout_id, ()) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, + _: &mut Self::RequestLayoutState, cx: &mut ElementContext, - ) -> Self::AfterLayout { + ) -> Self::PrepaintState { self.interactivity - .after_layout(bounds, bounds.size, cx, |_, _, hitbox, cx| { + .prepaint(bounds, bounds.size, cx, |_, _, hitbox, cx| { let hitbox = hitbox.unwrap(); let settings = ThemeSettings::get_global(cx).clone(); @@ -669,7 +669,7 @@ impl Element for TerminalElement { .id("terminal-element") .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) .into_any_element(); - element.layout(offset, bounds.size.into(), cx); + element.prepaint_as_root(offset, bounds.size.into(), cx); element }); @@ -775,8 +775,8 @@ impl Element for TerminalElement { fn paint( &mut self, bounds: Bounds, - _: &mut Self::BeforeLayout, - layout: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + layout: &mut Self::PrepaintState, cx: &mut ElementContext<'_>, ) { cx.paint_quad(fill(bounds, layout.background_color)); diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 6c16bc565d..a3a9eba8f2 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -168,10 +168,13 @@ pub struct PopoverMenuFrameState { } impl Element for PopoverMenu { - type BeforeLayout = PopoverMenuFrameState; - type AfterLayout = Option; + type RequestLayoutState = PopoverMenuFrameState; + type PrepaintState = Option; - fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) { + fn request_layout( + &mut self, + cx: &mut ElementContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { self.with_element_state(cx, |this, element_state, cx| { let mut menu_layout_id = None; @@ -186,7 +189,7 @@ impl Element for PopoverMenu { .with_priority(1) .into_any(); - menu_layout_id = Some(element.before_layout(cx)); + menu_layout_id = Some(element.request_layout(cx)); element }); @@ -196,7 +199,7 @@ impl Element for PopoverMenu { let child_layout_id = child_element .as_mut() - .map(|child_element| child_element.before_layout(cx)); + .map(|child_element| child_element.request_layout(cx)); let layout_id = cx.request_layout( &gpui::Style::default(), @@ -214,22 +217,22 @@ impl Element for PopoverMenu { }) } - fn after_layout( + fn prepaint( &mut self, _bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Option { self.with_element_state(cx, |_this, element_state, cx| { - if let Some(child) = before_layout.child_element.as_mut() { - child.after_layout(cx); + if let Some(child) = request_layout.child_element.as_mut() { + child.prepaint(cx); } - if let Some(menu) = before_layout.menu_element.as_mut() { - menu.after_layout(cx); + if let Some(menu) = request_layout.menu_element.as_mut() { + menu.prepaint(cx); } - before_layout.child_layout_id.map(|layout_id| { + request_layout.child_layout_id.map(|layout_id| { let bounds = cx.layout_bounds(layout_id); element_state.child_bounds = Some(bounds); cx.insert_hitbox(bounds, false).id @@ -240,16 +243,16 @@ impl Element for PopoverMenu { fn paint( &mut self, _: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, child_hitbox: &mut Option, cx: &mut ElementContext, ) { self.with_element_state(cx, |_this, _element_state, cx| { - if let Some(mut child) = before_layout.child_element.take() { + if let Some(mut child) = request_layout.child_element.take() { child.paint(cx); } - if let Some(mut menu) = before_layout.menu_element.take() { + if let Some(mut menu) = request_layout.menu_element.take() { menu.paint(cx); if let Some(child_hitbox) = *child_hitbox { diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index b14963d07d..5382236271 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -96,10 +96,13 @@ pub struct MenuHandleFrameState { } impl Element for RightClickMenu { - type BeforeLayout = MenuHandleFrameState; - type AfterLayout = Hitbox; + type RequestLayoutState = MenuHandleFrameState; + type PrepaintState = Hitbox; - fn before_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, Self::BeforeLayout) { + fn request_layout( + &mut self, + cx: &mut ElementContext, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { self.with_element_state(cx, |this, element_state, cx| { let mut menu_layout_id = None; @@ -114,7 +117,7 @@ impl Element for RightClickMenu { .with_priority(1) .into_any(); - menu_layout_id = Some(element.before_layout(cx)); + menu_layout_id = Some(element.request_layout(cx)); element }); @@ -125,7 +128,7 @@ impl Element for RightClickMenu { let child_layout_id = child_element .as_mut() - .map(|child_element| child_element.before_layout(cx)); + .map(|child_element| child_element.request_layout(cx)); let layout_id = cx.request_layout( &gpui::Style::default(), @@ -143,21 +146,21 @@ impl Element for RightClickMenu { }) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - before_layout: &mut Self::BeforeLayout, + request_layout: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> Hitbox { cx.with_element_id(Some(self.id.clone()), |cx| { let hitbox = cx.insert_hitbox(bounds, false); - if let Some(child) = before_layout.child_element.as_mut() { - child.after_layout(cx); + if let Some(child) = request_layout.child_element.as_mut() { + child.prepaint(cx); } - if let Some(menu) = before_layout.menu_element.as_mut() { - menu.after_layout(cx); + if let Some(menu) = request_layout.menu_element.as_mut() { + menu.prepaint(cx); } hitbox @@ -167,16 +170,16 @@ impl Element for RightClickMenu { fn paint( &mut self, _bounds: Bounds, - before_layout: &mut Self::BeforeLayout, - hitbox: &mut Self::AfterLayout, + request_layout: &mut Self::RequestLayoutState, + hitbox: &mut Self::PrepaintState, cx: &mut ElementContext, ) { self.with_element_state(cx, |this, element_state, cx| { - if let Some(mut child) = before_layout.child_element.take() { + if let Some(mut child) = request_layout.child_element.take() { child.paint(cx); } - if let Some(mut menu) = before_layout.menu_element.take() { + if let Some(mut menu) = request_layout.menu_element.take() { menu.paint(cx); return; } @@ -188,7 +191,7 @@ impl Element for RightClickMenu { let attach = this.attach; let menu = element_state.menu.clone(); let position = element_state.position.clone(); - let child_layout_id = before_layout.child_layout_id; + let child_layout_id = request_layout.child_layout_id; let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); let hitbox_id = hitbox.id; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 3897dca354..faf25457d9 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -792,13 +792,13 @@ mod element { } impl Element for PaneAxisElement { - type BeforeLayout = (); - type AfterLayout = PaneAxisLayout; + type RequestLayoutState = (); + type PrepaintState = PaneAxisLayout; - fn before_layout( + fn request_layout( &mut self, cx: &mut ui::prelude::ElementContext, - ) -> (gpui::LayoutId, Self::BeforeLayout) { + ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.flex_grow = 1.; style.flex_shrink = 1.; @@ -808,10 +808,10 @@ mod element { (cx.request_layout(&style, None), ()) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - _state: &mut Self::BeforeLayout, + _state: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) -> PaneAxisLayout { let dragged_handle = cx.with_element_state::>>, _>( @@ -872,7 +872,8 @@ mod element { size: child_size, }; bounding_boxes.push(Some(child_bounds)); - child.layout(origin, child_size.into(), cx); + child.layout_as_root(child_size.into(), cx); + child.prepaint_at(origin, cx); origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); layout.children.push(PaneAxisChildLayout { @@ -897,8 +898,8 @@ mod element { fn paint( &mut self, bounds: gpui::Bounds, - _: &mut Self::BeforeLayout, - layout: &mut Self::AfterLayout, + _: &mut Self::RequestLayoutState, + layout: &mut Self::PrepaintState, cx: &mut ui::prelude::ElementContext, ) { for child in &mut layout.children { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 303323f735..5f11c39446 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4971,10 +4971,10 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { struct DisconnectedOverlay; impl Element for DisconnectedOverlay { - type BeforeLayout = AnyElement; - type AfterLayout = (); + type RequestLayoutState = AnyElement; + type PrepaintState = (); - fn before_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::BeforeLayout) { + fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { let mut background = cx.theme().colors().elevated_surface_background; background.fade_out(0.2); let mut overlay = div() @@ -4992,24 +4992,24 @@ impl Element for DisconnectedOverlay { "Your connection to the remote project has been lost.", )) .into_any(); - (overlay.before_layout(cx), overlay) + (overlay.request_layout(cx), overlay) } - fn after_layout( + fn prepaint( &mut self, bounds: Bounds, - overlay: &mut Self::BeforeLayout, + overlay: &mut Self::RequestLayoutState, cx: &mut ElementContext, ) { cx.insert_hitbox(bounds, true); - overlay.after_layout(cx); + overlay.prepaint(cx); } fn paint( &mut self, _: Bounds, - overlay: &mut Self::BeforeLayout, - _: &mut Self::AfterLayout, + overlay: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, cx: &mut ElementContext, ) { overlay.paint(cx) From 2ee257a5621d3c2714945e117d958383c14ae513 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:27:18 +0200 Subject: [PATCH 023/101] task_ui: Move status indicator into tab bar of terminal panel (#10846) I'm not a huge fan of this change (& I expect the placement to change). The plan is to have the button in a toolbar of terminal panel, but I'm not sure if occupying a whole line of vertical space for a single button is worth it; I suppose we might want to put more of tasks ui inside of that toolbar. Release Notes: - Removed task status indicator and added "Spawn task" action to terminal panel context menu. --- Cargo.lock | 2 +- crates/tasks_ui/Cargo.toml | 1 - crates/tasks_ui/src/lib.rs | 5 +- crates/tasks_ui/src/modal.rs | 3 +- crates/tasks_ui/src/status_indicator.rs | 98 ---------------------- crates/terminal_view/Cargo.toml | 1 + crates/terminal_view/src/terminal_panel.rs | 50 ++++++++--- crates/workspace/src/pane.rs | 4 +- crates/zed/src/zed.rs | 2 - 9 files changed, 46 insertions(+), 120 deletions(-) delete mode 100644 crates/tasks_ui/src/status_indicator.rs diff --git a/Cargo.lock b/Cargo.lock index 52942e16b6..00b6922e07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9765,7 +9765,6 @@ dependencies = [ "serde_json", "settings", "task", - "terminal", "tree-sitter-rust", "tree-sitter-typescript", "ui", @@ -9863,6 +9862,7 @@ dependencies = [ "shellexpand", "smol", "task", + "tasks_ui", "terminal", "theme", "ui", diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index b19122f1de..67f62e2dcf 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -22,7 +22,6 @@ serde.workspace = true settings.workspace = true ui.workspace = true util.workspace = true -terminal.workspace = true workspace.workspace = true language.workspace = true diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 2433ca93ce..4898c8af0d 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -8,7 +8,7 @@ use anyhow::Context; use editor::Editor; use gpui::{AppContext, ViewContext, WindowContext}; use language::{BasicContextProvider, ContextProvider, Language}; -use modal::{Spawn, TasksModal}; +use modal::TasksModal; use project::{Location, TaskSourceKind, WorktreeId}; use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables}; use util::ResultExt; @@ -16,9 +16,8 @@ use workspace::Workspace; mod modal; mod settings; -mod status_indicator; -pub use status_indicator::TaskStatusIndicator; +pub use modal::Spawn; pub fn init(cx: &mut AppContext) { settings::TaskSettings::register(cx); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index db91c817f6..ab04cd5909 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -31,10 +31,11 @@ pub struct Spawn { } impl Spawn { - pub(crate) fn modal() -> Self { + pub fn modal() -> Self { Self { task_name: None } } } + /// Rerun last task #[derive(PartialEq, Clone, Deserialize, Default)] pub struct Rerun { diff --git a/crates/tasks_ui/src/status_indicator.rs b/crates/tasks_ui/src/status_indicator.rs deleted file mode 100644 index 8ed837cf48..0000000000 --- a/crates/tasks_ui/src/status_indicator.rs +++ /dev/null @@ -1,98 +0,0 @@ -use gpui::{IntoElement, Render, View, WeakView}; -use settings::Settings; -use ui::{ - div, ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, Tooltip, - VisualContext, WindowContext, -}; -use workspace::{item::ItemHandle, StatusItemView, Workspace}; - -use crate::{modal::Spawn, settings::TaskSettings}; - -enum TaskStatus { - Failed, - Running, - Succeeded, -} - -/// A status bar icon that surfaces the status of running tasks. -/// It has a different color depending on the state of running tasks: -/// - red if any open task tab failed -/// - else, yellow if any open task tab is still running -/// - else, green if there tasks tabs open, and they have all succeeded -/// - else, no indicator if there are no open task tabs -pub struct TaskStatusIndicator { - workspace: WeakView, -} - -impl TaskStatusIndicator { - pub fn new(workspace: WeakView, cx: &mut WindowContext) -> View { - cx.new_view(|_| Self { workspace }) - } - fn current_status(&self, cx: &mut WindowContext) -> Option { - self.workspace - .update(cx, |this, cx| { - let mut status = None; - let project = this.project().read(cx); - - for handle in project.local_terminal_handles() { - let Some(handle) = handle.upgrade() else { - continue; - }; - let handle = handle.read(cx); - let task_state = handle.task(); - if let Some(state) = task_state { - match state.status { - terminal::TaskStatus::Running => { - let _ = status.insert(TaskStatus::Running); - } - terminal::TaskStatus::Completed { success } => { - if !success { - let _ = status.insert(TaskStatus::Failed); - return status; - } - status.get_or_insert(TaskStatus::Succeeded); - } - _ => {} - }; - } - } - status - }) - .ok() - .flatten() - } -} - -impl Render for TaskStatusIndicator { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - if !TaskSettings::get_global(cx).show_status_indicator { - return div().into_any_element(); - } - let current_status = self.current_status(cx); - let color = current_status.map(|status| match status { - TaskStatus::Failed => Color::Error, - TaskStatus::Running => Color::Warning, - TaskStatus::Succeeded => Color::Success, - }); - IconButton::new("tasks-activity-indicator", IconName::Play) - .when_some(color, |this, color| this.icon_color(color)) - .on_click(cx.listener(|this, _, cx| { - this.workspace - .update(cx, |this, cx| { - crate::spawn_task_or_modal(this, &Spawn::modal(), cx) - }) - .ok(); - })) - .tooltip(|cx| Tooltip::for_action("Spawn tasks", &Spawn { task_name: None }, cx)) - .into_any_element() - } -} - -impl StatusItemView for TaskStatusIndicator { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _: &mut ui::prelude::ViewContext, - ) { - } -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index d9e3606d4d..f6e53bc22f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -24,6 +24,7 @@ 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 6f3fe0a15c..8682ac05b1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -5,9 +5,9 @@ use collections::{HashMap, HashSet}; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; use gpui::{ - actions, Action, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, - FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, Action, AppContext, AsyncWindowContext, DismissEvent, Entity, EventEmitter, + ExternalPaths, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; use project::{Fs, ProjectEntryId}; @@ -16,7 +16,10 @@ use serde::{Deserialize, Serialize}; use settings::Settings; use task::{RevealStrategy, SpawnInTerminal, TaskId}; use terminal::terminal_settings::{Shell, TerminalDockPosition, TerminalSettings}; -use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip}; +use ui::{ + h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, Selectable, + Tooltip, +}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -59,7 +62,6 @@ pub struct TerminalPanel { impl TerminalPanel { fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let terminal_panel = cx.view().downgrade(); let pane = cx.new_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -73,19 +75,43 @@ impl TerminalPanel { pane.set_can_navigate(false, cx); pane.display_nav_history_buttons(None); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - let terminal_panel = terminal_panel.clone(); h_flex() .gap_2() .child( IconButton::new("plus", IconName::Plus) .icon_size(IconSize::Small) - .on_click(move |_, cx| { - terminal_panel - .update(cx, |panel, cx| panel.add_terminal(None, None, cx)) - .log_err(); - }) - .tooltip(|cx| Tooltip::text("New Terminal", cx)), + .on_click(cx.listener(|pane, _, cx| { + let focus_handle = pane.focus_handle(cx); + let menu = ContextMenu::build(cx, |menu, _| { + menu.action( + "New Terminal", + workspace::NewTerminal.boxed_clone(), + ) + .entry( + "Spawn task", + Some(tasks_ui::Spawn::modal().boxed_clone()), + move |cx| { + // We want the focus to go back to terminal panel once task modal is dismissed, + // hence we focus that first. Otherwise, we'd end up without a focused element, as + // context menu will be gone the moment we spawn the modal. + cx.focus(&focus_handle); + cx.dispatch_action( + tasks_ui::Spawn::modal().boxed_clone(), + ); + }, + ) + }); + cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| { + pane.new_item_menu = None; + }) + .detach(); + pane.new_item_menu = Some(menu); + })) + .tooltip(|cx| Tooltip::text("New...", cx)), ) + .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Pane::render_menu_overlay(new_item_menu)) + }) .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 de561320fa..c4f62715b3 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -193,7 +193,7 @@ pub struct Pane { last_focus_handle_by_item: HashMap, nav_history: NavHistory, toolbar: View, - new_item_menu: Option>, + pub new_item_menu: Option>, split_item_menu: Option>, // tab_context_menu: View, pub(crate) workspace: WeakView, @@ -1747,7 +1747,7 @@ impl Pane { ) } - fn render_menu_overlay(menu: &View) -> Div { + pub fn render_menu_overlay(menu: &View) -> Div { div().absolute().bottom_0().right_0().size_0().child( deferred( anchored() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cf68f8a0e9..963b7c3237 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -131,7 +131,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); - let tasks_indicator = tasks_ui::TaskStatusIndicator::new(workspace.weak_handle(), cx); let active_buffer_language = cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); let vim_mode_indicator = cx.new_view(|cx| vim::ModeIndicator::new(cx)); @@ -141,7 +140,6 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(copilot, cx); - status_bar.add_right_item(tasks_indicator, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(vim_mode_indicator, cx); status_bar.add_right_item(cursor_position, cx); From 85b26e97886ef1e2b1dc10979ff31df97afa4d20 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 23 Apr 2024 19:13:04 +0200 Subject: [PATCH 024/101] Store goldenfiles with trailing newline (#10900) Release Notes: - N/A --- crates/git/src/blame.rs | 4 +++- crates/git/test_data/golden/blame_incremental_complex.json | 2 +- .../git/test_data/golden/blame_incremental_not_committed.json | 2 +- crates/git/test_data/golden/blame_incremental_simple.json | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index bb86cb4275..6b9848609d 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -336,8 +336,10 @@ mod tests { path.push("golden"); path.push(format!("{}.json", golden_filename)); - let have_json = + let mut have_json = serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); + // We always want to save with a trailing newline. + have_json.push('\n'); let update = std::env::var("UPDATE_GOLDEN") .map(|val| val.to_ascii_lowercase() == "true") diff --git a/crates/git/test_data/golden/blame_incremental_complex.json b/crates/git/test_data/golden/blame_incremental_complex.json index 84d49d847b..3eb6ec81e3 100644 --- a/crates/git/test_data/golden/blame_incremental_complex.json +++ b/crates/git/test_data/golden/blame_incremental_complex.json @@ -778,4 +778,4 @@ "previous": null, "filename": "crates/vim/src/utils.rs" } -] \ No newline at end of file +] diff --git a/crates/git/test_data/golden/blame_incremental_not_committed.json b/crates/git/test_data/golden/blame_incremental_not_committed.json index 0298fb05d3..4e4834d45c 100644 --- a/crates/git/test_data/golden/blame_incremental_not_committed.json +++ b/crates/git/test_data/golden/blame_incremental_not_committed.json @@ -132,4 +132,4 @@ "previous": null, "filename": "file_b.txt" } -] \ No newline at end of file +] diff --git a/crates/git/test_data/golden/blame_incremental_simple.json b/crates/git/test_data/golden/blame_incremental_simple.json index 4d6e9124d6..c8fba83897 100644 --- a/crates/git/test_data/golden/blame_incremental_simple.json +++ b/crates/git/test_data/golden/blame_incremental_simple.json @@ -132,4 +132,4 @@ "previous": null, "filename": "index.js" } -] \ No newline at end of file +] From f6eaa8b00fa45e1c79a9b81233746a5cb077c765 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 23 Apr 2024 13:31:21 -0400 Subject: [PATCH 025/101] Clean up whitespace (#10755) I saved the `file_types.json` file and got a diff because it had some trailing whitespace. I ran [`lineman`](https://github.com/JosephTLyons/lineman) on the codebase. I've done this before, but this time, I've added in the following settings to our `.zed` local settings, to make sure every future save respects our desire to have consistent whitespace formatting. ```json "remove_trailing_whitespace_on_save": true, "ensure_final_newline_on_save": true ``` Release Notes: - N/A --- .zed/settings.json | 4 +++- assets/icons/file_icons/file_types.json | 2 +- assets/icons/mail_open.svg | 2 +- assets/icons/trash.svg | 2 +- assets/icons/word_search.svg | 1 - crates/db/README.md | 2 +- crates/languages/src/bash/redactions.scm | 2 +- crates/languages/src/c/highlights.scm | 1 - crates/languages/src/c/injections.scm | 2 +- crates/languages/src/cpp/injections.scm | 2 +- crates/languages/src/javascript/injections.scm | 4 ++-- crates/languages/src/jsdoc/brackets.scm | 2 +- crates/languages/src/json/highlights.scm | 2 +- crates/languages/src/json/overrides.scm | 2 +- crates/languages/src/json/redactions.scm | 2 +- crates/languages/src/ruby/brackets.scm | 2 +- crates/languages/src/rust/brackets.scm | 2 +- crates/languages/src/rust/injections.scm | 2 +- crates/languages/src/tsx/injections.scm | 2 +- crates/languages/src/typescript/injections.scm | 2 +- crates/languages/src/yaml/highlights.scm | 6 +++--- crates/languages/src/yaml/outline.scm | 2 +- crates/terminal_view/scripts/truecolor.sh | 2 +- crates/vim/test_data/test_replace_mode_undo.json | 2 +- crates/zed/resources/windows/manifest.xml | 6 +++--- extensions/clojure/languages/clojure/outline.scm | 1 - extensions/html/languages/html/overrides.scm | 2 +- extensions/lua/languages/lua/brackets.scm | 2 +- extensions/lua/languages/lua/indents.scm | 2 +- extensions/lua/languages/lua/outline.scm | 2 +- extensions/ocaml/languages/ocaml/brackets.scm | 1 - extensions/ocaml/languages/ocaml/highlights.scm | 2 +- extensions/ocaml/languages/ocaml/indents.scm | 2 +- extensions/ocaml/languages/ocaml/outline.scm | 4 ++-- extensions/racket/languages/racket/outline.scm | 2 +- extensions/scheme/languages/scheme/outline.scm | 2 +- extensions/toml/languages/toml/outline.scm | 2 +- extensions/toml/languages/toml/redactions.scm | 2 +- script/licenses/template.hbs.md | 6 +++--- 39 files changed, 45 insertions(+), 47 deletions(-) diff --git a/.zed/settings.json b/.zed/settings.json index dbafa2115a..eedf2f3753 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -21,5 +21,7 @@ "formatter": "prettier" } }, - "formatter": "auto" + "formatter": "auto", + "remove_trailing_whitespace_on_save": true, + "ensure_final_newline_on_save": true } diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 5a587b02cf..0ee203c3c7 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -329,7 +329,7 @@ }, "tcl": { "icon": "icons/file_icons/tcl.svg" - }, + }, "vcs": { "icon": "icons/file_icons/git.svg" }, diff --git a/assets/icons/mail_open.svg b/assets/icons/mail_open.svg index b63915bd73..b857037b86 100644 --- a/assets/icons/mail_open.svg +++ b/assets/icons/mail_open.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index 94d7971f9b..b71035b99c 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/icons/word_search.svg b/assets/icons/word_search.svg index adb4976bcc..beca4cbe82 100644 --- a/assets/icons/word_search.svg +++ b/assets/icons/word_search.svg @@ -3,4 +3,3 @@ - diff --git a/crates/db/README.md b/crates/db/README.md index d4ea2fee39..b734a2e3a3 100644 --- a/crates/db/README.md +++ b/crates/db/README.md @@ -2,4 +2,4 @@ First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`. -To actually use and test your queries, import the generated DB file into https://sqliteonline.com/ \ No newline at end of file +To actually use and test your queries, import the generated DB file into https://sqliteonline.com/ diff --git a/crates/languages/src/bash/redactions.scm b/crates/languages/src/bash/redactions.scm index 88b38f42fc..000cb042a5 100644 --- a/crates/languages/src/bash/redactions.scm +++ b/crates/languages/src/bash/redactions.scm @@ -1,2 +1,2 @@ (variable_assignment - value: (_) @redact) \ No newline at end of file + value: (_) @redact) diff --git a/crates/languages/src/c/highlights.scm b/crates/languages/src/c/highlights.scm index 064ec61a37..0a8c12f06f 100644 --- a/crates/languages/src/c/highlights.scm +++ b/crates/languages/src/c/highlights.scm @@ -106,4 +106,3 @@ (primitive_type) (sized_type_specifier) ] @type - diff --git a/crates/languages/src/c/injections.scm b/crates/languages/src/c/injections.scm index 845a63bd1b..2696594af2 100644 --- a/crates/languages/src/c/injections.scm +++ b/crates/languages/src/c/injections.scm @@ -4,4 +4,4 @@ (preproc_function_def value: (preproc_arg) @content - (#set! "language" "c")) \ No newline at end of file + (#set! "language" "c")) diff --git a/crates/languages/src/cpp/injections.scm b/crates/languages/src/cpp/injections.scm index eca372d577..076703c809 100644 --- a/crates/languages/src/cpp/injections.scm +++ b/crates/languages/src/cpp/injections.scm @@ -4,4 +4,4 @@ (preproc_function_def value: (preproc_arg) @content - (#set! "language" "c++")) \ No newline at end of file + (#set! "language" "c++")) diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index 2003675245..0df1691e7e 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -1,5 +1,5 @@ ((comment) @content (#set! "language" "jsdoc")) - + ((regex) @content - (#set! "language" "regex")) \ No newline at end of file + (#set! "language" "regex")) diff --git a/crates/languages/src/jsdoc/brackets.scm b/crates/languages/src/jsdoc/brackets.scm index 24453c9da9..0e1bf5ca19 100644 --- a/crates/languages/src/jsdoc/brackets.scm +++ b/crates/languages/src/jsdoc/brackets.scm @@ -1,2 +1,2 @@ ("[" @open "]" @close) -("{" @open "}" @close) \ No newline at end of file +("{" @open "}" @close) diff --git a/crates/languages/src/json/highlights.scm b/crates/languages/src/json/highlights.scm index 12bde13e51..7116805109 100644 --- a/crates/languages/src/json/highlights.scm +++ b/crates/languages/src/json/highlights.scm @@ -18,4 +18,4 @@ "}" "[" "]" -] @punctuation.bracket \ No newline at end of file +] @punctuation.bracket diff --git a/crates/languages/src/json/overrides.scm b/crates/languages/src/json/overrides.scm index 746dbc5cd9..cc966ad4c1 100644 --- a/crates/languages/src/json/overrides.scm +++ b/crates/languages/src/json/overrides.scm @@ -1 +1 @@ -(string) @string \ No newline at end of file +(string) @string diff --git a/crates/languages/src/json/redactions.scm b/crates/languages/src/json/redactions.scm index be985f018c..7359637244 100644 --- a/crates/languages/src/json/redactions.scm +++ b/crates/languages/src/json/redactions.scm @@ -1,4 +1,4 @@ (pair value: (number) @redact) (pair value: (string) @redact) (array (number) @redact) -(array (string) @redact) \ No newline at end of file +(array (string) @redact) diff --git a/crates/languages/src/ruby/brackets.scm b/crates/languages/src/ruby/brackets.scm index 957b20ecdb..f5129f8f31 100644 --- a/crates/languages/src/ruby/brackets.scm +++ b/crates/languages/src/ruby/brackets.scm @@ -11,4 +11,4 @@ (begin "begin" @open "end" @close) (module "module" @open "end" @close) (_ . "def" @open "end" @close) -(_ . "class" @open "end" @close) \ No newline at end of file +(_ . "class" @open "end" @close) diff --git a/crates/languages/src/rust/brackets.scm b/crates/languages/src/rust/brackets.scm index 0be534c48c..eeee5f0e26 100644 --- a/crates/languages/src/rust/brackets.scm +++ b/crates/languages/src/rust/brackets.scm @@ -3,4 +3,4 @@ ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close) -(closure_parameters "|" @open "|" @close) \ No newline at end of file +(closure_parameters "|" @open "|" @close) diff --git a/crates/languages/src/rust/injections.scm b/crates/languages/src/rust/injections.scm index 57ebea8539..0ce91f2287 100644 --- a/crates/languages/src/rust/injections.scm +++ b/crates/languages/src/rust/injections.scm @@ -4,4 +4,4 @@ (macro_rule (token_tree) @content - (#set! "language" "rust")) \ No newline at end of file + (#set! "language" "rust")) diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 8aca54dbd2..0df1691e7e 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -2,4 +2,4 @@ (#set! "language" "jsdoc")) ((regex) @content - (#set! "language" "regex")) \ No newline at end of file + (#set! "language" "regex")) diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 8aca54dbd2..0df1691e7e 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -2,4 +2,4 @@ (#set! "language" "jsdoc")) ((regex) @content - (#set! "language" "regex")) \ No newline at end of file + (#set! "language" "regex")) diff --git a/crates/languages/src/yaml/highlights.scm b/crates/languages/src/yaml/highlights.scm index 06081f63cb..0e66aca66d 100644 --- a/crates/languages/src/yaml/highlights.scm +++ b/crates/languages/src/yaml/highlights.scm @@ -20,10 +20,10 @@ [ (anchor_name) (alias_name) - (tag) + (tag) ] @type -key: (flow_node (plain_scalar (string_scalar) @property)) +key: (flow_node (plain_scalar (string_scalar) @property)) [ "," @@ -46,4 +46,4 @@ key: (flow_node (plain_scalar (string_scalar) @property)) "&" "---" "..." -] @punctuation.special \ No newline at end of file +] @punctuation.special diff --git a/crates/languages/src/yaml/outline.scm b/crates/languages/src/yaml/outline.scm index e85eb1bf8a..7ab007835f 100644 --- a/crates/languages/src/yaml/outline.scm +++ b/crates/languages/src/yaml/outline.scm @@ -1 +1 @@ -(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item \ No newline at end of file +(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item diff --git a/crates/terminal_view/scripts/truecolor.sh b/crates/terminal_view/scripts/truecolor.sh index 14e5d81308..c11037b100 100755 --- a/crates/terminal_view/scripts/truecolor.sh +++ b/crates/terminal_view/scripts/truecolor.sh @@ -16,4 +16,4 @@ awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1} if (colnum%term_cols==term_cols) printf "\n"; } printf "\n"; -}' \ No newline at end of file +}' diff --git a/crates/vim/test_data/test_replace_mode_undo.json b/crates/vim/test_data/test_replace_mode_undo.json index 7628a27fb4..3488030ee7 100644 --- a/crates/vim/test_data/test_replace_mode_undo.json +++ b/crates/vim/test_data/test_replace_mode_undo.json @@ -121,4 +121,4 @@ {"Key":"backspace"} {"Key":"backspace"} {"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy ˇdog.","mode":"Replace"}} \ No newline at end of file +{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy ˇdog.","mode":"Replace"}} diff --git a/crates/zed/resources/windows/manifest.xml b/crates/zed/resources/windows/manifest.xml index 5490c54d07..5a69b43486 100644 --- a/crates/zed/resources/windows/manifest.xml +++ b/crates/zed/resources/windows/manifest.xml @@ -7,9 +7,9 @@ - diff --git a/extensions/clojure/languages/clojure/outline.scm b/extensions/clojure/languages/clojure/outline.scm index 8b13789179..e69de29bb2 100644 --- a/extensions/clojure/languages/clojure/outline.scm +++ b/extensions/clojure/languages/clojure/outline.scm @@ -1 +0,0 @@ - diff --git a/extensions/html/languages/html/overrides.scm b/extensions/html/languages/html/overrides.scm index 97accffd67..7108d48fbd 100644 --- a/extensions/html/languages/html/overrides.scm +++ b/extensions/html/languages/html/overrides.scm @@ -1,2 +1,2 @@ (comment) @comment -(quoted_attribute_value) @string \ No newline at end of file +(quoted_attribute_value) @string diff --git a/extensions/lua/languages/lua/brackets.scm b/extensions/lua/languages/lua/brackets.scm index 5f5bd60b93..62e137ef26 100644 --- a/extensions/lua/languages/lua/brackets.scm +++ b/extensions/lua/languages/lua/brackets.scm @@ -1,3 +1,3 @@ ("[" @open "]" @close) ("{" @open "}" @close) -("(" @open ")" @close) \ No newline at end of file +("(" @open ")" @close) diff --git a/extensions/lua/languages/lua/indents.scm b/extensions/lua/languages/lua/indents.scm index 71e15a0c33..ed26c5a8f0 100644 --- a/extensions/lua/languages/lua/indents.scm +++ b/extensions/lua/languages/lua/indents.scm @@ -7,4 +7,4 @@ (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent \ No newline at end of file +(_ "(" ")" @end) @indent diff --git a/extensions/lua/languages/lua/outline.scm b/extensions/lua/languages/lua/outline.scm index 8bd8d88070..aa59d17247 100644 --- a/extensions/lua/languages/lua/outline.scm +++ b/extensions/lua/languages/lua/outline.scm @@ -1,3 +1,3 @@ (function_declaration "function" @context - name: (_) @name) @item \ No newline at end of file + name: (_) @name) @item diff --git a/extensions/ocaml/languages/ocaml/brackets.scm b/extensions/ocaml/languages/ocaml/brackets.scm index 6afe4638fd..269d87778d 100644 --- a/extensions/ocaml/languages/ocaml/brackets.scm +++ b/extensions/ocaml/languages/ocaml/brackets.scm @@ -4,4 +4,3 @@ ("{" @open "}" @close) ("<" @open ">" @close) ("\"" @open "\"" @close) - diff --git a/extensions/ocaml/languages/ocaml/highlights.scm b/extensions/ocaml/languages/ocaml/highlights.scm index 41db5a403e..6623e1e543 100644 --- a/extensions/ocaml/languages/ocaml/highlights.scm +++ b/extensions/ocaml/languages/ocaml/highlights.scm @@ -131,7 +131,7 @@ (extension) (item_extension) (quoted_extension) - (quoted_item_extension) + (quoted_item_extension) "%" ] @attribute diff --git a/extensions/ocaml/languages/ocaml/indents.scm b/extensions/ocaml/languages/ocaml/indents.scm index 10995d15ab..319d2fd971 100644 --- a/extensions/ocaml/languages/ocaml/indents.scm +++ b/extensions/ocaml/languages/ocaml/indents.scm @@ -3,7 +3,7 @@ (type_binding) (method_definition) - + (external) (value_specification) (method_specification) diff --git a/extensions/ocaml/languages/ocaml/outline.scm b/extensions/ocaml/languages/ocaml/outline.scm index 16f449664a..c7f39c219b 100644 --- a/extensions/ocaml/languages/ocaml/outline.scm +++ b/extensions/ocaml/languages/ocaml/outline.scm @@ -17,7 +17,7 @@ "module" @context "type" @context name: (_) @name) @item - + (type_definition "type" @context (type_binding name: (_) @name)) @item @@ -25,7 +25,7 @@ (value_specification "val" @context (value_name) @name) @item - + (class_definition "class" @context (class_binding diff --git a/extensions/racket/languages/racket/outline.scm b/extensions/racket/languages/racket/outline.scm index 604e052a63..6001548303 100644 --- a/extensions/racket/languages/racket/outline.scm +++ b/extensions/racket/languages/racket/outline.scm @@ -7,4 +7,4 @@ (list . (symbol) @name) ] (#match? @start-symbol "^define") -) @item \ No newline at end of file +) @item diff --git a/extensions/scheme/languages/scheme/outline.scm b/extensions/scheme/languages/scheme/outline.scm index 604e052a63..6001548303 100644 --- a/extensions/scheme/languages/scheme/outline.scm +++ b/extensions/scheme/languages/scheme/outline.scm @@ -7,4 +7,4 @@ (list . (symbol) @name) ] (#match? @start-symbol "^define") -) @item \ No newline at end of file +) @item diff --git a/extensions/toml/languages/toml/outline.scm b/extensions/toml/languages/toml/outline.scm index d232d489b6..0b37949628 100644 --- a/extensions/toml/languages/toml/outline.scm +++ b/extensions/toml/languages/toml/outline.scm @@ -12,4 +12,4 @@ (pair . - (_) @name) @item \ No newline at end of file + (_) @name) @item diff --git a/extensions/toml/languages/toml/redactions.scm b/extensions/toml/languages/toml/redactions.scm index fd11a02927..a906e9ac7b 100644 --- a/extensions/toml/languages/toml/redactions.scm +++ b/extensions/toml/languages/toml/redactions.scm @@ -1 +1 @@ -(pair (bare_key) "=" (_) @redact) \ No newline at end of file +(pair (bare_key) "=" (_) @redact) diff --git a/script/licenses/template.hbs.md b/script/licenses/template.hbs.md index a41aee8a4c..cc986588fb 100644 --- a/script/licenses/template.hbs.md +++ b/script/licenses/template.hbs.md @@ -8,14 +8,14 @@ {{#each licenses}} #### {{name}} - + ##### Used by: {{#each used_by}} * [{{crate.name}} {{crate.version}}]({{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}) {{/each}} - + {{text}} -------------------------------------------------------------------------------- -{{/each}} \ No newline at end of file +{{/each}} From 8ae4c3277f3096d99093dd49970492dd02d6d597 Mon Sep 17 00:00:00 2001 From: Michael Angerman <1809991+stormasm@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:45:12 -0700 Subject: [PATCH 026/101] storybook: Fix crash in Kitchen Sink and Auto Height Editor stories (#10904) The *Kitchen Sink* as well as the *Auto Height Editor* story is crashing for the same reason that the Picker story was crashing... ### Related Topics - Picker Story PR : #10793 - Picker Story Issue : #10739 - Introduced By : #10620 Release Notes: - N/A --- crates/storybook/src/stories/picker.rs | 2 -- crates/storybook/src/storybook.rs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs index 1aceeea9b2..ca156ba730 100644 --- a/crates/storybook/src/stories/picker.rs +++ b/crates/storybook/src/stories/picker.rs @@ -1,7 +1,6 @@ use fuzzy::StringMatchCandidate; use gpui::{div, prelude::*, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext}; use picker::{Picker, PickerDelegate}; -use project::Project; use std::sync::Arc; use ui::{prelude::*, ListItemSpacing}; use ui::{Label, ListItem}; @@ -191,7 +190,6 @@ impl PickerStory { ]); delegate.update_matches("".into(), cx).detach(); - Project::init_settings(cx); let picker = Picker::uniform_list(delegate, cx); picker.focus(cx); picker diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 77b0a38c3d..015b4765fb 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -10,6 +10,7 @@ use gpui::{ div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowOptions, }; use log::LevelFilter; +use project::Project; use settings::{default_settings, KeymapFile, Settings, SettingsStore}; use simplelog::SimpleLogger; use strum::IntoEnumIterator; @@ -80,6 +81,7 @@ fn main() { language::init(cx); editor::init(cx); + Project::init_settings(cx); init(cx); load_storybook_keymap(cx); cx.set_menus(app_menus()); From e0c83a1d32c013c404db39b9739a931c69a948d9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 23 Apr 2024 15:33:09 -0600 Subject: [PATCH 027/101] remote projects per user (#10594) Release Notes: - Made remote projects per-user instead of per-channel. If you'd like to be part of the remote development alpha, please email hi@zed.dev. --------- Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Co-authored-by: Bennet Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com> Co-authored-by: Nate Butler --- Cargo.lock | 23 +- Cargo.toml | 2 + assets/icons/server.svg | 19 +- crates/call/src/room.rs | 24 +- crates/channel/src/channel.rs | 4 +- crates/channel/src/channel_store.rs | 206 +---- crates/client/src/user.rs | 4 +- crates/collab/Cargo.toml | 1 + .../20221109000000_test_schema.sql | 9 +- .../20240412165156_dev_servers_per_user.sql | 7 + ...192746_unique_remote_projects_by_paths.sql | 3 + crates/collab/src/db.rs | 6 +- crates/collab/src/db/queries/channels.rs | 5 - crates/collab/src/db/queries/dev_servers.rs | 100 ++- crates/collab/src/db/queries/projects.rs | 118 ++- .../collab/src/db/queries/remote_projects.rs | 98 ++- crates/collab/src/db/queries/rooms.rs | 88 +- crates/collab/src/db/tables/dev_server.rs | 16 +- crates/collab/src/db/tables/remote_project.rs | 18 +- crates/collab/src/db/tests/db_tests.rs | 6 +- crates/collab/src/rpc.rs | 296 ++++--- crates/collab/src/rpc/connection_pool.rs | 10 +- crates/collab/src/tests/channel_tests.rs | 2 + crates/collab/src/tests/dev_server_tests.rs | 323 +++++++- crates/collab/src/tests/integration_tests.rs | 4 + crates/collab/src/tests/test_server.rs | 1 + crates/collab_ui/Cargo.toml | 1 - crates/collab_ui/src/channel_view.rs | 4 - crates/collab_ui/src/collab_panel.rs | 148 +--- .../src/collab_panel/dev_server_modal.rs | 622 --------------- crates/collab_ui/src/collab_titlebar_item.rs | 108 +-- crates/editor/src/items.rs | 1 + crates/headless/src/headless.rs | 73 +- .../src/highlighted_match_with_paths.rs | 11 +- crates/project/src/project.rs | 38 +- crates/recent_projects/Cargo.toml | 7 + crates/recent_projects/src/recent_projects.rs | 303 +++++-- crates/recent_projects/src/remote_projects.rs | 749 ++++++++++++++++++ crates/remote_projects/Cargo.toml | 23 + crates/remote_projects/src/remote_projects.rs | 186 +++++ crates/rpc/proto/zed.proto | 44 +- crates/rpc/src/proto.rs | 7 +- crates/sqlez/src/connection.rs | 99 ++- crates/sqlez/src/statement.rs | 1 + crates/tasks_ui/src/modal.rs | 1 + crates/ui/src/components/icon.rs | 64 +- crates/ui/src/components/modal.rs | 41 +- crates/ui_text_field/src/ui_text_field.rs | 35 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/item.rs | 8 +- crates/workspace/src/persistence.rs | 279 +++++-- crates/workspace/src/persistence/model.rs | 105 ++- crates/workspace/src/workspace.rs | 73 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + script/zed-local | 5 +- 56 files changed, 2807 insertions(+), 1625 deletions(-) create mode 100644 crates/collab/migrations/20240412165156_dev_servers_per_user.sql create mode 100644 crates/collab/migrations/20240417192746_unique_remote_projects_by_paths.sql delete mode 100644 crates/collab_ui/src/collab_panel/dev_server_modal.rs create mode 100644 crates/recent_projects/src/remote_projects.rs create mode 100644 crates/remote_projects/Cargo.toml create mode 100644 crates/remote_projects/src/remote_projects.rs diff --git a/Cargo.lock b/Cargo.lock index 00b6922e07..3d22a64359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2254,6 +2254,7 @@ dependencies = [ "prost", "rand 0.8.5", "release_channel", + "remote_projects", "reqwest", "rpc", "rustc-demangle", @@ -2299,7 +2300,6 @@ dependencies = [ "editor", "emojis", "extensions_ui", - "feature_flags", "futures 0.3.28", "fuzzy", "gpui", @@ -7728,7 +7728,9 @@ dependencies = [ name = "recent_projects" version = "0.1.0" dependencies = [ + "anyhow", "editor", + "feature_flags", "fuzzy", "gpui", "language", @@ -7736,10 +7738,15 @@ dependencies = [ "ordered-float 2.10.0", "picker", "project", + "remote_projects", + "rpc", "serde", "serde_json", + "settings", "smol", + "theme", "ui", + "ui_text_field", "util", "workspace", ] @@ -7866,6 +7873,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "remote_projects" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "gpui", + "rpc", + "serde", + "serde_json", +] + [[package]] name = "rend" version = "0.4.0" @@ -12303,6 +12322,7 @@ dependencies = [ "parking_lot", "postage", "project", + "remote_projects", "schemars", "serde", "serde_json", @@ -12601,6 +12621,7 @@ dependencies = [ "quick_action_bar", "recent_projects", "release_channel", + "remote_projects", "rope", "search", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4b9bad5554..d2ff0c5066 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ members = [ "crates/refineable", "crates/refineable/derive_refineable", "crates/release_channel", + "crates/remote_projects", "crates/rich_text", "crates/rope", "crates/rpc", @@ -200,6 +201,7 @@ project_symbols = { path = "crates/project_symbols" } quick_action_bar = { path = "crates/quick_action_bar" } recent_projects = { path = "crates/recent_projects" } release_channel = { path = "crates/release_channel" } +remote_projects = { path = "crates/remote_projects" } rich_text = { path = "crates/rich_text" } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } diff --git a/assets/icons/server.svg b/assets/icons/server.svg index 10fbdcbff4..a8b6ad92b3 100644 --- a/assets/icons/server.svg +++ b/assets/icons/server.svg @@ -1,5 +1,16 @@ - - - - + + + + + diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 7ec80334e4..22940537d5 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1203,14 +1203,24 @@ impl Room { project: Model, cx: &mut ModelContext, ) -> Task> { - if let Some(project_id) = project.read(cx).remote_id() { - return Task::ready(Ok(project_id)); - } + let request = if let Some(remote_project_id) = project.read(cx).remote_project_id() { + self.client.request(proto::ShareProject { + room_id: self.id(), + worktrees: vec![], + remote_project_id: Some(remote_project_id.0), + }) + } else { + if let Some(project_id) = project.read(cx).remote_id() { + return Task::ready(Ok(project_id)); + } + + self.client.request(proto::ShareProject { + room_id: self.id(), + worktrees: project.read(cx).worktree_metadata_protos(cx), + remote_project_id: None, + }) + }; - let request = self.client.request(proto::ShareProject { - room_id: self.id(), - worktrees: project.read(cx).worktree_metadata_protos(cx), - }); cx.spawn(|this, mut cx| async move { let response = request.await?; diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index f592c1f8e7..aee92d0f6c 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -11,9 +11,7 @@ pub use channel_chat::{ mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, }; -pub use channel_store::{ - Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject, -}; +pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 0d323a2fa0..7b07c7a530 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,10 +3,7 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; use anyhow::{anyhow, Result}; use channel_index::ChannelIndex; -use client::{ - ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User, - UserId, UserStore, -}; +use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{ @@ -15,7 +12,7 @@ use gpui::{ }; use language::Capability; use rpc::{ - proto::{self, ChannelRole, ChannelVisibility, DevServerStatus}, + proto::{self, ChannelRole, ChannelVisibility}, TypedEnvelope, }; use settings::Settings; @@ -53,57 +50,12 @@ impl From for HostedProject { } } } - -#[derive(Debug, Clone)] -pub struct RemoteProject { - pub id: RemoteProjectId, - pub project_id: Option, - pub channel_id: ChannelId, - pub name: SharedString, - pub path: SharedString, - pub dev_server_id: DevServerId, -} - -impl From for RemoteProject { - fn from(project: proto::RemoteProject) -> Self { - Self { - id: RemoteProjectId(project.id), - project_id: project.project_id.map(|id| ProjectId(id)), - channel_id: ChannelId(project.channel_id), - name: project.name.into(), - path: project.path.into(), - dev_server_id: DevServerId(project.dev_server_id), - } - } -} - -#[derive(Debug, Clone)] -pub struct DevServer { - pub id: DevServerId, - pub channel_id: ChannelId, - pub name: SharedString, - pub status: DevServerStatus, -} - -impl From for DevServer { - fn from(dev_server: proto::DevServer) -> Self { - Self { - id: DevServerId(dev_server.dev_server_id), - channel_id: ChannelId(dev_server.channel_id), - status: dev_server.status(), - name: dev_server.name.into(), - } - } -} - pub struct ChannelStore { pub channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, channel_states: HashMap, hosted_projects: HashMap, - remote_projects: HashMap, - dev_servers: HashMap, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, @@ -133,8 +85,6 @@ pub struct ChannelState { observed_chat_message: Option, role: Option, projects: HashSet, - dev_servers: HashSet, - remote_projects: HashSet, } impl Channel { @@ -265,8 +215,6 @@ impl ChannelStore { channel_index: ChannelIndex::default(), channel_participants: Default::default(), hosted_projects: Default::default(), - remote_projects: Default::default(), - dev_servers: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), opened_chats: Default::default(), @@ -366,40 +314,6 @@ impl ChannelStore { projects } - pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec { - let mut dev_servers: Vec = self - .channel_states - .get(&channel_id) - .map(|state| state.dev_servers.clone()) - .unwrap_or_default() - .into_iter() - .flat_map(|id| self.dev_servers.get(&id).cloned()) - .collect(); - dev_servers.sort_by_key(|s| (s.name.clone(), s.id)); - dev_servers - } - - pub fn find_dev_server_by_id(&self, id: DevServerId) -> Option<&DevServer> { - self.dev_servers.get(&id) - } - - pub fn find_remote_project_by_id(&self, id: RemoteProjectId) -> Option<&RemoteProject> { - self.remote_projects.get(&id) - } - - pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec { - let mut remote_projects: Vec = self - .channel_states - .get(&channel_id) - .map(|state| state.remote_projects.clone()) - .unwrap_or_default() - .into_iter() - .flat_map(|id| self.remote_projects.get(&id).cloned()) - .collect(); - remote_projects.sort_by_key(|p| (p.name.clone(), p.id)); - remote_projects - } - pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool { if let Some(buffer) = self.opened_buffers.get(&channel_id) { if let OpenedModelHandle::Open(buffer) = buffer { @@ -901,46 +815,6 @@ impl ChannelStore { Ok(()) }) } - - pub fn create_remote_project( - &mut self, - channel_id: ChannelId, - dev_server_id: DevServerId, - name: String, - path: String, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.background_executor().spawn(async move { - client - .request(proto::CreateRemoteProject { - channel_id: channel_id.0, - dev_server_id: dev_server_id.0, - name, - path, - }) - .await - }) - } - - pub fn create_dev_server( - &mut self, - channel_id: ChannelId, - name: String, - cx: &mut ModelContext, - ) -> Task> { - let client = self.client.clone(); - cx.background_executor().spawn(async move { - let result = client - .request(proto::CreateDevServer { - channel_id: channel_id.0, - name, - }) - .await?; - Ok(result) - }) - } - pub fn get_channel_member_details( &self, channel_id: ChannelId, @@ -1221,11 +1095,7 @@ impl ChannelStore { || !payload.latest_channel_message_ids.is_empty() || !payload.latest_channel_buffer_versions.is_empty() || !payload.hosted_projects.is_empty() - || !payload.deleted_hosted_projects.is_empty() - || !payload.dev_servers.is_empty() - || !payload.deleted_dev_servers.is_empty() - || !payload.remote_projects.is_empty() - || !payload.deleted_remote_projects.is_empty(); + || !payload.deleted_hosted_projects.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { @@ -1313,60 +1183,6 @@ impl ChannelStore { .remove_hosted_project(old_project.project_id); } } - - for remote_project in payload.remote_projects { - let remote_project: RemoteProject = remote_project.into(); - if let Some(old_remote_project) = self - .remote_projects - .insert(remote_project.id, remote_project.clone()) - { - self.channel_states - .entry(old_remote_project.channel_id) - .or_default() - .remove_remote_project(old_remote_project.id); - } - self.channel_states - .entry(remote_project.channel_id) - .or_default() - .add_remote_project(remote_project.id); - } - - for remote_project_id in payload.deleted_remote_projects { - let remote_project_id = RemoteProjectId(remote_project_id); - - if let Some(old_project) = self.remote_projects.remove(&remote_project_id) { - self.channel_states - .entry(old_project.channel_id) - .or_default() - .remove_remote_project(old_project.id); - } - } - - for dev_server in payload.dev_servers { - let dev_server: DevServer = dev_server.into(); - if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone()) - { - self.channel_states - .entry(old_server.channel_id) - .or_default() - .remove_dev_server(old_server.id); - } - self.channel_states - .entry(dev_server.channel_id) - .or_default() - .add_dev_server(dev_server.id); - } - - for dev_server_id in payload.deleted_dev_servers { - let dev_server_id = DevServerId(dev_server_id); - - if let Some(old_server) = self.dev_servers.remove(&dev_server_id) { - self.channel_states - .entry(old_server.channel_id) - .or_default() - .remove_dev_server(old_server.id); - } - } } cx.notify(); @@ -1481,20 +1297,4 @@ impl ChannelState { fn remove_hosted_project(&mut self, project_id: ProjectId) { self.projects.remove(&project_id); } - - fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) { - self.remote_projects.insert(remote_project_id); - } - - fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) { - self.remote_projects.remove(&remote_project_id); - } - - fn add_dev_server(&mut self, dev_server_id: DevServerId) { - self.dev_servers.insert(dev_server_id); - } - - fn remove_dev_server(&mut self, dev_server_id: DevServerId) { - self.dev_servers.remove(&dev_server_id); - } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 2c5632593d..5479b73c71 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -30,7 +30,9 @@ pub struct ProjectId(pub u64); #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub struct DevServerId(pub u64); -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] pub struct RemoteProjectId(pub u64); #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8f1a125cb7..1b58438e9a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -93,6 +93,7 @@ notifications = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } release_channel.workspace = true +remote_projects.workspace = true rpc = { workspace = true, features = ["test-support"] } sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] } serde_json.workspace = true diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index bc14721e21..c9d064edec 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -398,26 +398,21 @@ CREATE TABLE hosted_projects ( channel_id INTEGER NOT NULL REFERENCES channels(id), name TEXT NOT NULL, visibility TEXT NOT NULL, - deleted_at TIMESTAMP NULL, - dev_server_id INTEGER REFERENCES dev_servers(id), - dev_server_path TEXT + deleted_at TIMESTAMP NULL ); CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id); CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL); CREATE TABLE dev_servers ( id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL REFERENCES channels(id), + user_id INTEGER NOT NULL REFERENCES users(id), name TEXT NOT NULL, hashed_token TEXT NOT NULL ); -CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id); CREATE TABLE remote_projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id INTEGER NOT NULL REFERENCES channels(id), dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id), - name TEXT NOT NULL, path TEXT NOT NULL ); diff --git a/crates/collab/migrations/20240412165156_dev_servers_per_user.sql b/crates/collab/migrations/20240412165156_dev_servers_per_user.sql new file mode 100644 index 0000000000..7ef9e2fde0 --- /dev/null +++ b/crates/collab/migrations/20240412165156_dev_servers_per_user.sql @@ -0,0 +1,7 @@ +DELETE FROM remote_projects; +DELETE FROM dev_servers; + +ALTER TABLE dev_servers DROP COLUMN channel_id; +ALTER TABLE dev_servers ADD COLUMN user_id INT NOT NULL REFERENCES users(id); + +ALTER TABLE remote_projects DROP COLUMN channel_id; diff --git a/crates/collab/migrations/20240417192746_unique_remote_projects_by_paths.sql b/crates/collab/migrations/20240417192746_unique_remote_projects_by_paths.sql new file mode 100644 index 0000000000..923b948cee --- /dev/null +++ b/crates/collab/migrations/20240417192746_unique_remote_projects_by_paths.sql @@ -0,0 +1,3 @@ +ALTER TABLE remote_projects DROP COLUMN name; +ALTER TABLE remote_projects +ADD CONSTRAINT unique_path_constraint UNIQUE(dev_server_id, path); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 24bae3fba7..4a7ae9197a 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -655,8 +655,6 @@ pub struct ChannelsForUser { pub channel_memberships: Vec, pub channel_participants: HashMap>, pub hosted_projects: Vec, - pub dev_servers: Vec, - pub remote_projects: Vec, pub observed_buffer_versions: Vec, pub observed_channel_messages: Vec, @@ -764,6 +762,7 @@ pub struct Project { pub collaborators: Vec, pub worktrees: BTreeMap, pub language_servers: Vec, + pub remote_project_id: Option, } pub struct ProjectCollaborator { @@ -786,8 +785,7 @@ impl ProjectCollaborator { #[derive(Debug)] pub struct LeftProject { pub id: ProjectId, - pub host_user_id: Option, - pub host_connection_id: Option, + pub should_unshare: bool, pub connection_ids: Vec, } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 279f767df8..3f168e0854 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -640,15 +640,10 @@ impl Database { .get_hosted_projects(&channel_ids, &roles_by_channel_id, tx) .await?; - let dev_servers = self.get_dev_servers(&channel_ids, tx).await?; - let remote_projects = self.get_remote_projects(&channel_ids, tx).await?; - Ok(ChannelsForUser { channel_memberships, channels, hosted_projects, - dev_servers, - remote_projects, channel_participants, latest_buffer_versions, latest_channel_messages, diff --git a/crates/collab/src/db/queries/dev_servers.rs b/crates/collab/src/db/queries/dev_servers.rs index 4767f24734..ceb7d905da 100644 --- a/crates/collab/src/db/queries/dev_servers.rs +++ b/crates/collab/src/db/queries/dev_servers.rs @@ -1,6 +1,9 @@ -use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter}; +use rpc::proto; +use sea_orm::{ + ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter, +}; -use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId}; +use super::{dev_server, remote_project, Database, DevServerId, UserId}; impl Database { pub async fn get_dev_server( @@ -16,40 +19,105 @@ impl Database { .await } - pub async fn get_dev_servers( + pub async fn get_dev_servers(&self, user_id: UserId) -> crate::Result> { + self.transaction(|tx| async move { + Ok(dev_server::Entity::find() + .filter(dev_server::Column::UserId.eq(user_id)) + .all(&*tx) + .await?) + }) + .await + } + + pub async fn remote_projects_update( &self, - channel_ids: &Vec, + user_id: UserId, + ) -> crate::Result { + self.transaction( + |tx| async move { self.remote_projects_update_internal(user_id, &tx).await }, + ) + .await + } + + pub async fn remote_projects_update_internal( + &self, + user_id: UserId, tx: &DatabaseTransaction, - ) -> crate::Result> { - let servers = dev_server::Entity::find() - .filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0))) + ) -> crate::Result { + let dev_servers = dev_server::Entity::find() + .filter(dev_server::Column::UserId.eq(user_id)) .all(tx) .await?; - Ok(servers) + + let remote_projects = remote_project::Entity::find() + .filter( + remote_project::Column::DevServerId + .is_in(dev_servers.iter().map(|d| d.id).collect::>()), + ) + .find_also_related(super::project::Entity) + .all(tx) + .await?; + + Ok(proto::RemoteProjectsUpdate { + dev_servers: dev_servers + .into_iter() + .map(|d| d.to_proto(proto::DevServerStatus::Offline)) + .collect(), + remote_projects: remote_projects + .into_iter() + .map(|(remote_project, project)| remote_project.to_proto(project)) + .collect(), + }) } pub async fn create_dev_server( &self, - channel_id: ChannelId, name: &str, hashed_access_token: &str, user_id: UserId, - ) -> crate::Result<(channel::Model, dev_server::Model)> { + ) -> crate::Result<(dev_server::Model, proto::RemoteProjectsUpdate)> { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_admin(&channel, user_id, &tx) - .await?; - let dev_server = dev_server::Entity::insert(dev_server::ActiveModel { id: ActiveValue::NotSet, hashed_token: ActiveValue::Set(hashed_access_token.to_string()), - channel_id: ActiveValue::Set(channel_id), name: ActiveValue::Set(name.to_string()), + user_id: ActiveValue::Set(user_id), }) .exec_with_returning(&*tx) .await?; - Ok((channel, dev_server)) + let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?; + + Ok((dev_server, remote_projects)) + }) + .await + } + + pub async fn delete_dev_server( + &self, + id: DevServerId, + user_id: UserId, + ) -> crate::Result { + self.transaction(|tx| async move { + let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else { + return Err(anyhow::anyhow!("no dev server with id {}", id))?; + }; + if dev_server.user_id != user_id { + return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?; + } + + remote_project::Entity::delete_many() + .filter(remote_project::Column::DevServerId.eq(id)) + .exec(&*tx) + .await?; + + dev_server::Entity::delete(dev_server.into_active_model()) + .exec(&*tx) + .await?; + + let remote_projects = self.remote_projects_update_internal(user_id, &tx).await?; + + Ok(remote_projects) }) .await } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 03b8b5d29e..94a083698c 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -30,6 +30,7 @@ impl Database { room_id: RoomId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], + remote_project_id: Option, ) -> Result> { self.room_transaction(room_id, |tx| async move { let participant = room_participant::Entity::find() @@ -58,6 +59,30 @@ impl Database { return Err(anyhow!("guests cannot share projects"))?; } + if let Some(remote_project_id) = remote_project_id { + let project = project::Entity::find() + .filter(project::Column::RemoteProjectId.eq(Some(remote_project_id))) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no remote project"))?; + + if project.room_id.is_some() { + return Err(anyhow!("project already shared"))?; + }; + + let project = project::Entity::update(project::ActiveModel { + room_id: ActiveValue::Set(Some(room_id)), + ..project.into_active_model() + }) + .exec(&*tx) + .await?; + + // todo! check user is a project-collaborator + + let room = self.get_room(room_id, &tx).await?; + return Ok((project.id, room)); + } + let project = project::ActiveModel { room_id: ActiveValue::set(Some(participant.room_id)), host_user_id: ActiveValue::set(Some(participant.user_id)), @@ -111,6 +136,7 @@ impl Database { &self, project_id: ProjectId, connection: ConnectionId, + user_id: Option, ) -> Result, Vec)>> { self.project_transaction(project_id, |tx| async move { let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; @@ -118,19 +144,37 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("project not found"))?; + let room = if let Some(room_id) = project.room_id { + Some(self.get_room(room_id, &tx).await?) + } else { + None + }; if project.host_connection()? == connection { - let room = if let Some(room_id) = project.room_id { - Some(self.get_room(room_id, &tx).await?) - } else { - None - }; project::Entity::delete(project.into_active_model()) .exec(&*tx) .await?; - Ok((room, guest_connection_ids)) - } else { - Err(anyhow!("cannot unshare a project hosted by another user"))? + return Ok((room, guest_connection_ids)); } + if let Some(remote_project_id) = project.remote_project_id { + if let Some(user_id) = user_id { + if user_id + != self + .owner_for_remote_project(remote_project_id, &tx) + .await? + { + Err(anyhow!("cannot unshare a project hosted by another user"))? + } + project::Entity::update(project::ActiveModel { + room_id: ActiveValue::Set(None), + ..project.into_active_model() + }) + .exec(&*tx) + .await?; + return Ok((room, guest_connection_ids)); + } + } + + Err(anyhow!("cannot unshare a project hosted by another user"))? }) .await } @@ -753,6 +797,7 @@ impl Database { name: language_server.name, }) .collect(), + remote_project_id: project.remote_project_id, }; Ok((project, replica_id as ReplicaId)) } @@ -794,8 +839,7 @@ impl Database { Ok(LeftProject { id: project.id, connection_ids, - host_user_id: None, - host_connection_id: None, + should_unshare: false, }) }) .await @@ -832,7 +876,7 @@ impl Database { .find_related(project_collaborator::Entity) .all(&*tx) .await?; - let connection_ids = collaborators + let connection_ids: Vec = collaborators .into_iter() .map(|collaborator| collaborator.connection()) .collect(); @@ -870,8 +914,7 @@ impl Database { let left_project = LeftProject { id: project_id, - host_user_id: project.host_user_id, - host_connection_id: Some(project.host_connection()?), + should_unshare: connection == project.host_connection()?, connection_ids, }; Ok((room, left_project)) @@ -914,7 +957,7 @@ impl Database { capability: Capability, tx: &DatabaseTransaction, ) -> Result<(project::Model, ChannelRole)> { - let (project, remote_project) = project::Entity::find_by_id(project_id) + let (mut project, remote_project) = project::Entity::find_by_id(project_id) .find_also_related(remote_project::Entity) .one(tx) .await? @@ -933,27 +976,44 @@ impl Database { PrincipalId::UserId(user_id) => user_id, }; - let role = if let Some(remote_project) = remote_project { - let channel = channel::Entity::find_by_id(remote_project.channel_id) - .one(tx) - .await? - .ok_or_else(|| anyhow!("no such channel"))?; - - self.check_user_is_channel_participant(&channel, user_id, &tx) - .await? - } else if let Some(room_id) = project.room_id { - // what's the users role? - let current_participant = room_participant::Entity::find() + let role_from_room = if let Some(room_id) = project.room_id { + room_participant::Entity::find() .filter(room_participant::Column::RoomId.eq(room_id)) .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) .one(tx) .await? - .ok_or_else(|| anyhow!("no such room"))?; - - current_participant.role.unwrap_or(ChannelRole::Guest) + .and_then(|participant| participant.role) } else { - return Err(anyhow!("not authorized to read projects"))?; + None }; + let role_from_remote_project = if let Some(remote_project) = remote_project { + let dev_server = dev_server::Entity::find_by_id(remote_project.dev_server_id) + .one(tx) + .await? + .ok_or_else(|| anyhow!("no such channel"))?; + if user_id == dev_server.user_id { + // If the user left the room "uncleanly" they may rejoin the + // remote project before leave_room runs. IN that case kick + // the project out of the room pre-emptively. + if role_from_room.is_none() { + project = project::Entity::update(project::ActiveModel { + room_id: ActiveValue::Set(None), + ..project.into_active_model() + }) + .exec(tx) + .await?; + } + Some(ChannelRole::Admin) + } else { + None + } + } else { + None + }; + + let role = role_from_remote_project + .or(role_from_room) + .unwrap_or(ChannelRole::Banned); match capability { Capability::ReadWrite => { diff --git a/crates/collab/src/db/queries/remote_projects.rs b/crates/collab/src/db/queries/remote_projects.rs index 86538d219e..9baf9ad0c8 100644 --- a/crates/collab/src/db/queries/remote_projects.rs +++ b/crates/collab/src/db/queries/remote_projects.rs @@ -8,8 +8,8 @@ use sea_orm::{ use crate::db::ProjectId; use super::{ - channel, project, project_collaborator, remote_project, worktree, ChannelId, Database, - DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId, + dev_server, project, project_collaborator, remote_project, worktree, Database, DevServerId, + RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId, }; impl Database { @@ -26,29 +26,6 @@ impl Database { .await } - pub async fn get_remote_projects( - &self, - channel_ids: &Vec, - tx: &DatabaseTransaction, - ) -> crate::Result> { - let servers = remote_project::Entity::find() - .filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0))) - .find_also_related(project::Entity) - .all(tx) - .await?; - Ok(servers - .into_iter() - .map(|(remote_project, project)| proto::RemoteProject { - id: remote_project.id.to_proto(), - project_id: project.map(|p| p.id.to_proto()), - channel_id: remote_project.channel_id.to_proto(), - name: remote_project.name, - dev_server_id: remote_project.dev_server_id.to_proto(), - path: remote_project.path, - }) - .collect()) - } - pub async fn get_remote_projects_for_dev_server( &self, dev_server_id: DevServerId, @@ -64,8 +41,6 @@ impl Database { .map(|(remote_project, project)| proto::RemoteProject { id: remote_project.id.to_proto(), project_id: project.map(|p| p.id.to_proto()), - channel_id: remote_project.channel_id.to_proto(), - name: remote_project.name, dev_server_id: remote_project.dev_server_id.to_proto(), path: remote_project.path, }) @@ -74,6 +49,38 @@ impl Database { .await } + pub async fn remote_project_ids_for_user( + &self, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> crate::Result> { + let dev_servers = dev_server::Entity::find() + .filter(dev_server::Column::UserId.eq(user_id)) + .find_with_related(remote_project::Entity) + .all(tx) + .await?; + + Ok(dev_servers + .into_iter() + .flat_map(|(_, projects)| projects.into_iter().map(|p| p.id)) + .collect()) + } + + pub async fn owner_for_remote_project( + &self, + remote_project_id: RemoteProjectId, + tx: &DatabaseTransaction, + ) -> crate::Result { + let dev_server = remote_project::Entity::find_by_id(remote_project_id) + .find_also_related(dev_server::Entity) + .one(tx) + .await? + .and_then(|(_, dev_server)| dev_server) + .ok_or_else(|| anyhow!("no remote project"))?; + + Ok(dev_server.user_id) + } + pub async fn get_stale_dev_server_projects( &self, connection: ConnectionId, @@ -95,28 +102,30 @@ impl Database { pub async fn create_remote_project( &self, - channel_id: ChannelId, dev_server_id: DevServerId, - name: &str, path: &str, user_id: UserId, - ) -> crate::Result<(channel::Model, remote_project::Model)> { + ) -> crate::Result<(remote_project::Model, proto::RemoteProjectsUpdate)> { self.transaction(|tx| async move { - let channel = self.get_channel_internal(channel_id, &tx).await?; - self.check_user_is_channel_admin(&channel, user_id, &tx) - .await?; + let dev_server = dev_server::Entity::find_by_id(dev_server_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?; + if dev_server.user_id != user_id { + return Err(anyhow!("not your dev server"))?; + } let project = remote_project::Entity::insert(remote_project::ActiveModel { - name: ActiveValue::Set(name.to_string()), id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_id), dev_server_id: ActiveValue::Set(dev_server_id), path: ActiveValue::Set(path.to_string()), }) .exec_with_returning(&*tx) .await?; - Ok((channel, project)) + let status = self.remote_projects_update_internal(user_id, &tx).await?; + + Ok((project, status)) }) .await } @@ -127,8 +136,13 @@ impl Database { dev_server_id: DevServerId, connection: ConnectionId, worktrees: &[proto::WorktreeMetadata], - ) -> crate::Result { + ) -> crate::Result<(proto::RemoteProject, UserId, proto::RemoteProjectsUpdate)> { self.transaction(|tx| async move { + let dev_server = dev_server::Entity::find_by_id(dev_server_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no dev server with id {}", dev_server_id))?; + let remote_project = remote_project::Entity::find_by_id(remote_project_id) .one(&*tx) .await? @@ -168,7 +182,15 @@ impl Database { .await?; } - Ok(remote_project.to_proto(Some(project))) + let status = self + .remote_projects_update_internal(dev_server.user_id, &tx) + .await?; + + Ok(( + remote_project.to_proto(Some(project)), + dev_server.user_id, + status, + )) }) .await } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 46552740f3..9cd22666eb 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -849,11 +849,32 @@ impl Database { .into_values::<_, QueryProjectIds>() .all(&*tx) .await?; + + // if any project in the room has a remote-project-id that belongs to a dev server that this user owns. + let remote_projects_for_user = self + .remote_project_ids_for_user(leaving_participant.user_id, &tx) + .await?; + + let remote_projects_to_unshare = project::Entity::find() + .filter( + Condition::all() + .add(project::Column::RoomId.eq(room_id)) + .add( + project::Column::RemoteProjectId + .is_in(remote_projects_for_user.clone()), + ), + ) + .all(&*tx) + .await? + .into_iter() + .map(|project| project.id) + .collect::>(); let mut left_projects = HashMap::default(); let mut collaborators = project_collaborator::Entity::find() .filter(project_collaborator::Column::ProjectId.is_in(project_ids)) .stream(&*tx) .await?; + while let Some(collaborator) = collaborators.next().await { let collaborator = collaborator?; let left_project = @@ -861,9 +882,8 @@ impl Database { .entry(collaborator.project_id) .or_insert(LeftProject { id: collaborator.project_id, - host_user_id: Default::default(), connection_ids: Default::default(), - host_connection_id: None, + should_unshare: false, }); let collaborator_connection_id = collaborator.connection(); @@ -871,9 +891,10 @@ impl Database { left_project.connection_ids.push(collaborator_connection_id); } - if collaborator.is_host { - left_project.host_user_id = Some(collaborator.user_id); - left_project.host_connection_id = Some(collaborator_connection_id); + if (collaborator.is_host && collaborator.connection() == connection) + || remote_projects_to_unshare.contains(&collaborator.project_id) + { + left_project.should_unshare = true; } } drop(collaborators); @@ -915,6 +936,17 @@ impl Database { .exec(&*tx) .await?; + if !remote_projects_to_unshare.is_empty() { + project::Entity::update_many() + .filter(project::Column::Id.is_in(remote_projects_to_unshare)) + .set(project::ActiveModel { + room_id: ActiveValue::Set(None), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + let (channel, room) = self.get_channel_room(room_id, &tx).await?; let deleted = if room.participants.is_empty() { let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?; @@ -1264,38 +1296,46 @@ impl Database { } drop(db_participants); - let mut db_projects = db_room + let db_projects = db_room .find_related(project::Entity) .find_with_related(worktree::Entity) - .stream(tx) + .all(tx) .await?; - while let Some(row) = db_projects.next().await { - let (db_project, db_worktree) = row?; + for (db_project, db_worktrees) in db_projects { let host_connection = db_project.host_connection()?; if let Some(participant) = participants.get_mut(&host_connection) { - let project = if let Some(project) = participant - .projects - .iter_mut() - .find(|project| project.id == db_project.id.to_proto()) - { - project - } else { - participant.projects.push(proto::ParticipantProject { - id: db_project.id.to_proto(), - worktree_root_names: Default::default(), - }); - participant.projects.last_mut().unwrap() - }; + participant.projects.push(proto::ParticipantProject { + id: db_project.id.to_proto(), + worktree_root_names: Default::default(), + }); + let project = participant.projects.last_mut().unwrap(); - if let Some(db_worktree) = db_worktree { + for db_worktree in db_worktrees { if db_worktree.visible { project.worktree_root_names.push(db_worktree.root_name); } } + } else if let Some(remote_project_id) = db_project.remote_project_id { + let host = self.owner_for_remote_project(remote_project_id, tx).await?; + if let Some((_, participant)) = participants + .iter_mut() + .find(|(_, v)| v.user_id == host.to_proto()) + { + participant.projects.push(proto::ParticipantProject { + id: db_project.id.to_proto(), + worktree_root_names: Default::default(), + }); + let project = participant.projects.last_mut().unwrap(); + + for db_worktree in db_worktrees { + if db_worktree.visible { + project.worktree_root_names.push(db_worktree.root_name); + } + } + } } } - drop(db_projects); let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?; let mut followers = Vec::new(); diff --git a/crates/collab/src/db/tables/dev_server.rs b/crates/collab/src/db/tables/dev_server.rs index cd98ae4892..053db808a4 100644 --- a/crates/collab/src/db/tables/dev_server.rs +++ b/crates/collab/src/db/tables/dev_server.rs @@ -1,4 +1,4 @@ -use crate::db::{ChannelId, DevServerId}; +use crate::db::{DevServerId, UserId}; use rpc::proto; use sea_orm::entity::prelude::*; @@ -8,20 +8,28 @@ pub struct Model { #[sea_orm(primary_key)] pub id: DevServerId, pub name: String, - pub channel_id: ChannelId, + pub user_id: UserId, pub hashed_token: String, } impl ActiveModelBehavior for ActiveModel {} #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::remote_project::Entity")] + RemoteProject, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::RemoteProject.def() + } +} impl Model { pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer { proto::DevServer { dev_server_id: self.id.to_proto(), - channel_id: self.channel_id.to_proto(), name: self.name.clone(), status: status as i32, } diff --git a/crates/collab/src/db/tables/remote_project.rs b/crates/collab/src/db/tables/remote_project.rs index ba486d9733..a3c2b25725 100644 --- a/crates/collab/src/db/tables/remote_project.rs +++ b/crates/collab/src/db/tables/remote_project.rs @@ -1,5 +1,5 @@ use super::project; -use crate::db::{ChannelId, DevServerId, RemoteProjectId}; +use crate::db::{DevServerId, RemoteProjectId}; use rpc::proto; use sea_orm::entity::prelude::*; @@ -8,9 +8,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: RemoteProjectId, - pub channel_id: ChannelId, pub dev_server_id: DevServerId, - pub name: String, pub path: String, } @@ -20,6 +18,12 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::project::Entity")] Project, + #[sea_orm( + belongs_to = "super::dev_server::Entity", + from = "Column::DevServerId", + to = "super::dev_server::Column::Id" + )] + DevServer, } impl Related for Entity { @@ -28,14 +32,18 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::DevServer.def() + } +} + impl Model { pub fn to_proto(&self, project: Option) -> proto::RemoteProject { proto::RemoteProject { id: self.id.to_proto(), project_id: project.map(|p| p.id.to_proto()), - channel_id: self.channel_id.to_proto(), dev_server_id: self.dev_server_id.to_proto(), - name: self.name.clone(), path: self.path.clone(), } } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 96e0898709..c78ba9ec91 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -535,18 +535,18 @@ async fn test_project_count(db: &Arc) { .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None) .await .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[], None) .await .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) + db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[], None) .await .unwrap(); assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index da8328c411..3cba88b543 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -255,6 +255,13 @@ impl DevServerSession { pub fn dev_server_id(&self) -> DevServerId { self.0.dev_server_id().unwrap() } + + fn dev_server(&self) -> &dev_server::Model { + match &self.0.principal { + Principal::DevServer(dev_server) => dev_server, + _ => unreachable!(), + } + } } impl Deref for DevServerSession { @@ -405,6 +412,7 @@ impl Server { .add_request_handler(user_handler(rejoin_remote_projects)) .add_request_handler(user_handler(create_remote_project)) .add_request_handler(user_handler(create_dev_server)) + .add_request_handler(user_handler(delete_dev_server)) .add_request_handler(dev_server_handler(share_remote_project)) .add_request_handler(dev_server_handler(shutdown_dev_server)) .add_request_handler(dev_server_handler(reconnect_dev_server)) @@ -1044,12 +1052,14 @@ impl Server { .await?; } - let (contacts, channels_for_user, channel_invites) = future::try_join3( - self.app_state.db.get_contacts(user.id), - self.app_state.db.get_channels_for_user(user.id), - self.app_state.db.get_channel_invites_for_user(user.id), - ) - .await?; + let (contacts, channels_for_user, channel_invites, remote_projects) = + future::try_join4( + self.app_state.db.get_contacts(user.id), + self.app_state.db.get_channels_for_user(user.id), + self.app_state.db.get_channel_invites_for_user(user.id), + self.app_state.db.remote_projects_update(user.id), + ) + .await?; { let mut pool = self.connection_pool.lock(); @@ -1067,9 +1077,10 @@ impl Server { )?; self.peer.send( connection_id, - build_channels_update(channels_for_user, channel_invites, &pool), + build_channels_update(channels_for_user, channel_invites), )?; } + send_remote_projects_update(user.id, remote_projects, session).await; if let Some(incoming_call) = self.app_state.db.incoming_call_for_user(user.id).await? @@ -1087,9 +1098,6 @@ impl Server { }; pool.add_dev_server(connection_id, dev_server.id, zed_version); } - update_dev_server_status(dev_server, proto::DevServerStatus::Online, &session) - .await; - // todo!() allow only one connection. let projects = self .app_state @@ -1098,6 +1106,13 @@ impl Server { .await?; self.peer .send(connection_id, proto::DevServerInstructions { projects })?; + + let status = self + .app_state + .db + .remote_projects_update(dev_server.user_id) + .await?; + send_remote_projects_update(dev_server.user_id, status, &session).await; } } @@ -1401,10 +1416,8 @@ async fn connection_lost( update_user_contacts(session.user_id(), &session).await?; }, - Principal::DevServer(dev_server) => { - lost_dev_server_connection(&session).await?; - update_dev_server_status(&dev_server, proto::DevServerStatus::Offline, &session) - .await; + Principal::DevServer(_) => { + lost_dev_server_connection(&session.for_dev_server().unwrap()).await?; }, } }, @@ -1941,6 +1954,9 @@ async fn share_project( RoomId::from_proto(request.room_id), session.connection_id, &request.worktrees, + request + .remote_project_id + .map(|id| RemoteProjectId::from_proto(id)), ) .await?; response.send(proto::ShareProjectResponse { @@ -1954,14 +1970,25 @@ async fn share_project( /// Unshare a project from the room. async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(message.project_id); - unshare_project_internal(project_id, &session).await + unshare_project_internal( + project_id, + session.connection_id, + session.user_id(), + &session, + ) + .await } -async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> Result<()> { +async fn unshare_project_internal( + project_id: ProjectId, + connection_id: ConnectionId, + user_id: Option, + session: &Session, +) -> Result<()> { let (room, guest_connection_ids) = &*session .db() .await - .unshare_project(project_id, session.connection_id) + .unshare_project(project_id, connection_id, user_id) .await?; let message = proto::UnshareProject { @@ -1969,7 +1996,7 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R }; broadcast( - Some(session.connection_id), + Some(connection_id), guest_connection_ids.iter().copied(), |conn_id| session.peer.send(conn_id, message.clone()), ); @@ -1980,13 +2007,13 @@ async fn unshare_project_internal(project_id: ProjectId, session: &Session) -> R Ok(()) } -/// Share a project into the room. +/// DevServer makes a project available online async fn share_remote_project( request: proto::ShareRemoteProject, response: Response, session: DevServerSession, ) -> Result<()> { - let remote_project = session + let (remote_project, user_id, status) = session .db() .await .share_remote_project( @@ -2000,22 +2027,7 @@ async fn share_remote_project( return Err(anyhow!("failed to share remote project"))?; }; - for (connection_id, _) in session - .connection_pool() - .await - .channel_connection_ids(ChannelId::from_proto(remote_project.channel_id)) - { - session - .peer - .send( - connection_id, - proto::UpdateChannels { - remote_projects: vec![remote_project.clone()], - ..Default::default() - }, - ) - .trace_err(); - } + send_remote_projects_update(user_id, status, &session).await; response.send(proto::ShareProjectResponse { project_id })?; @@ -2081,19 +2093,21 @@ fn join_project_internal( }) .collect::>(); + let add_project_collaborator = proto::AddProjectCollaborator { + project_id: project_id.to_proto(), + collaborator: Some(proto::Collaborator { + peer_id: Some(session.connection_id.into()), + replica_id: replica_id.0 as u32, + user_id: guest_user_id.to_proto(), + }), + }; + for collaborator in &collaborators { session .peer .send( collaborator.peer_id.unwrap().into(), - proto::AddProjectCollaborator { - project_id: project_id.to_proto(), - collaborator: Some(proto::Collaborator { - peer_id: Some(session.connection_id.into()), - replica_id: replica_id.0 as u32, - user_id: guest_user_id.to_proto(), - }), - }, + add_project_collaborator.clone(), ) .trace_err(); } @@ -2105,7 +2119,10 @@ fn join_project_internal( replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), - role: project.role.into(), // todo + role: project.role.into(), + remote_project_id: project + .remote_project_id + .map(|remote_project_id| remote_project_id.0 as u64), })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -2188,8 +2205,6 @@ async fn leave_project(request: proto::LeaveProject, session: UserSession) -> Re let (room, project) = &*db.leave_project(project_id, sender_id).await?; tracing::info!( %project_id, - host_user_id = ?project.host_user_id, - host_connection_id = ?project.host_connection_id, "leave project" ); @@ -2224,13 +2239,33 @@ async fn create_remote_project( response: Response, session: UserSession, ) -> Result<()> { - let (channel, remote_project) = session + let dev_server_id = DevServerId(request.dev_server_id as i32); + let dev_server_connection_id = session + .connection_pool() + .await + .dev_server_connection_id(dev_server_id); + let Some(dev_server_connection_id) = dev_server_connection_id else { + Err(ErrorCode::DevServerOffline + .message("Cannot create a remote project when the dev server is offline".to_string()) + .anyhow())? + }; + + let path = request.path.clone(); + //Check that the path exists on the dev server + session + .peer + .forward_request( + session.connection_id, + dev_server_connection_id, + proto::ValidateRemoteProjectRequest { path: path.clone() }, + ) + .await?; + + let (remote_project, update) = session .db() .await .create_remote_project( - ChannelId(request.channel_id as i32), DevServerId(request.dev_server_id as i32), - &request.name, &request.path, session.user_id(), ) @@ -2242,25 +2277,12 @@ async fn create_remote_project( .get_remote_projects_for_dev_server(remote_project.dev_server_id) .await?; - let update = proto::UpdateChannels { - remote_projects: vec![remote_project.to_proto(None)], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) { - if role.can_see_all_descendants() { - session.peer.send(connection_id, update.clone())?; - } - } + session.peer.send( + dev_server_connection_id, + proto::DevServerInstructions { projects }, + )?; - let dev_server_id = remote_project.dev_server_id; - let dev_server_connection_id = connection_pool.dev_server_connection_id(dev_server_id); - if let Some(dev_server_connection_id) = dev_server_connection_id { - session.peer.send( - dev_server_connection_id, - proto::DevServerInstructions { projects }, - )?; - } + send_remote_projects_update(session.user_id(), update, &session).await; response.send(proto::CreateRemoteProjectResponse { remote_project: Some(remote_project.to_proto(None)), @@ -2276,37 +2298,56 @@ async fn create_dev_server( let access_token = auth::random_token(); let hashed_access_token = auth::hash_access_token(&access_token); - let (channel, dev_server) = session + let (dev_server, status) = session .db() .await - .create_dev_server( - ChannelId(request.channel_id as i32), - &request.name, - &hashed_access_token, - session.user_id(), - ) + .create_dev_server(&request.name, &hashed_access_token, session.user_id()) .await?; - let update = proto::UpdateChannels { - dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) { - if role.can_see_channel(channel.visibility) { - session.peer.send(connection_id, update.clone())?; - } - } + send_remote_projects_update(session.user_id(), status, &session).await; response.send(proto::CreateDevServerResponse { dev_server_id: dev_server.id.0 as u64, - channel_id: request.channel_id, access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token), name: request.name.clone(), })?; Ok(()) } +async fn delete_dev_server( + request: proto::DeleteDevServer, + response: Response, + session: UserSession, +) -> Result<()> { + let dev_server_id = DevServerId(request.dev_server_id as i32); + let dev_server = session.db().await.get_dev_server(dev_server_id).await?; + if dev_server.user_id != session.user_id() { + return Err(anyhow!(ErrorCode::Forbidden))?; + } + + let connection_id = session + .connection_pool() + .await + .dev_server_connection_id(dev_server_id); + if let Some(connection_id) = connection_id { + shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?; + session + .peer + .send(connection_id, proto::ShutdownDevServer {})?; + } + + let status = session + .db() + .await + .delete_dev_server(dev_server_id, session.user_id()) + .await?; + + send_remote_projects_update(session.user_id(), status, &session).await; + + response.send(proto::Ack {})?; + Ok(()) +} + async fn rejoin_remote_projects( request: proto::RejoinRemoteProjects, response: Response, @@ -2403,8 +2444,15 @@ async fn shutdown_dev_server( session: DevServerSession, ) -> Result<()> { response.send(proto::Ack {})?; + shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await +} + +async fn shutdown_dev_server_internal( + dev_server_id: DevServerId, + connection_id: ConnectionId, + session: &Session, +) -> Result<()> { let (remote_projects, dev_server) = { - let dev_server_id = session.dev_server_id(); let db = session.db().await; let remote_projects = db.get_remote_projects_for_dev_server(dev_server_id).await?; let dev_server = db.get_dev_server(dev_server_id).await?; @@ -2412,22 +2460,26 @@ async fn shutdown_dev_server( }; for project_id in remote_projects.iter().filter_map(|p| p.project_id) { - unshare_project_internal(ProjectId::from_proto(project_id), &session.0).await?; + unshare_project_internal( + ProjectId::from_proto(project_id), + connection_id, + None, + session, + ) + .await?; } - let update = proto::UpdateChannels { - remote_projects, - dev_servers: vec![dev_server.to_proto(proto::DevServerStatus::Offline)], - ..Default::default() - }; - - for (connection_id, _) in session + session .connection_pool() .await - .channel_connection_ids(dev_server.channel_id) - { - session.peer.send(connection_id, update.clone()).trace_err(); - } + .set_dev_server_offline(dev_server_id); + + let status = session + .db() + .await + .remote_projects_update(dev_server.user_id) + .await?; + send_remote_projects_update(dev_server.user_id, status, &session).await; Ok(()) } @@ -4626,7 +4678,7 @@ fn notify_membership_updated( ..Default::default() }; - let mut update = build_channels_update(result.new_channels, vec![], connection_pool); + let mut update = build_channels_update(result.new_channels, vec![]); update.delete_channels = result .removed_channels .into_iter() @@ -4659,7 +4711,6 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh fn build_channels_update( channels: ChannelsForUser, channel_invites: Vec, - pool: &ConnectionPool, ) -> proto::UpdateChannels { let mut update = proto::UpdateChannels::default(); @@ -4684,13 +4735,6 @@ fn build_channels_update( } update.hosted_projects = channels.hosted_projects; - update.dev_servers = channels - .dev_servers - .into_iter() - .map(|dev_server| dev_server.to_proto(pool.dev_server_status(dev_server.id))) - .collect(); - update.remote_projects = channels.remote_projects; - update } @@ -4777,24 +4821,19 @@ fn channel_updated( ); } -async fn update_dev_server_status( - dev_server: &dev_server::Model, - status: proto::DevServerStatus, +async fn send_remote_projects_update( + user_id: UserId, + mut status: proto::RemoteProjectsUpdate, session: &Session, ) { let pool = session.connection_pool().await; - let connections = pool.channel_connection_ids(dev_server.channel_id); - for (connection_id, _) in connections { - session - .peer - .send( - connection_id, - proto::UpdateChannels { - dev_servers: vec![dev_server.to_proto(status)], - ..Default::default() - }, - ) - .trace_err(); + for dev_server in &mut status.dev_servers { + dev_server.status = + pool.dev_server_status(DevServerId(dev_server.dev_server_id as i32)) as i32; + } + let connections = pool.user_connection_ids(user_id); + for connection_id in connections { + session.peer.send(connection_id, status.clone()).trace_err(); } } @@ -4833,7 +4872,7 @@ async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> Ok(()) } -async fn lost_dev_server_connection(session: &Session) -> Result<()> { +async fn lost_dev_server_connection(session: &DevServerSession) -> Result<()> { log::info!("lost dev server connection, unsharing projects"); let project_ids = session .db() @@ -4843,9 +4882,14 @@ async fn lost_dev_server_connection(session: &Session) -> Result<()> { for project_id in project_ids { // not unshare re-checks the connection ids match, so we get away with no transaction - unshare_project_internal(project_id, &session).await?; + unshare_project_internal(project_id, session.connection_id, None, &session).await?; } + let user_id = session.dev_server().user_id; + let update = session.db().await.remote_projects_update(user_id).await?; + + send_remote_projects_update(user_id, update, session).await; + Ok(()) } @@ -4947,7 +4991,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { fn project_left(project: &db::LeftProject, session: &UserSession) { for connection_id in &project.connection_ids { - if project.host_user_id == Some(session.user_id()) { + if project.should_unshare { session .peer .send( diff --git a/crates/collab/src/rpc/connection_pool.rs b/crates/collab/src/rpc/connection_pool.rs index 856fc616a3..5a7632f391 100644 --- a/crates/collab/src/rpc/connection_pool.rs +++ b/crates/collab/src/rpc/connection_pool.rs @@ -13,6 +13,7 @@ pub struct ConnectionPool { connected_users: BTreeMap, connected_dev_servers: BTreeMap, channels: ChannelPool, + offline_dev_servers: HashSet, } #[derive(Default, Serialize)] @@ -106,12 +107,17 @@ impl ConnectionPool { } PrincipalId::DevServerId(dev_server_id) => { self.connected_dev_servers.remove(&dev_server_id); + self.offline_dev_servers.remove(&dev_server_id); } } self.connections.remove(&connection_id).unwrap(); Ok(()) } + pub fn set_dev_server_offline(&mut self, dev_server_id: DevServerId) { + self.offline_dev_servers.insert(dev_server_id); + } + pub fn connections(&self) -> impl Iterator { self.connections.values() } @@ -137,7 +143,9 @@ impl ConnectionPool { } pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus { - if self.dev_server_connection_id(dev_server_id).is_some() { + if self.dev_server_connection_id(dev_server_id).is_some() + && !self.offline_dev_servers.contains(&dev_server_id) + { proto::DevServerStatus::Online } else { proto::DevServerStatus::Offline diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 14b0a87485..ae0035f3a2 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1023,6 +1023,8 @@ async fn test_channel_link_notifications( .await .unwrap(); + executor.run_until_parked(); + // the new channel shows for b and c assert_channels_list_shape( client_a.channel_store(), diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 91849b4fb9..40ecc66bd7 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -1,45 +1,40 @@ -use std::path::Path; +use std::{path::Path, sync::Arc}; +use call::ActiveCall; use editor::Editor; use fs::Fs; -use gpui::VisualTestContext; -use rpc::proto::DevServerStatus; +use gpui::{TestAppContext, VisualTestContext, WindowHandle}; +use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt}; use serde_json::json; +use workspace::{AppState, Workspace}; -use crate::tests::TestServer; +use crate::tests::{following_tests::join_channel, TestServer}; + +use super::TestClient; #[gpui::test] async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) { let (server, client) = TestServer::start1(cx).await; - let channel_id = server - .make_channel("test", None, (&client, cx), &mut []) - .await; + let store = cx.update(|cx| remote_projects::Store::global(cx).clone()); - let resp = client - .channel_store() + let resp = store .update(cx, |store, cx| { - store.create_dev_server(channel_id, "server-1".to_string(), cx) + store.create_dev_server("server-1".to_string(), cx) }) .await .unwrap(); - client.channel_store().update(cx, |store, _| { - assert_eq!(store.dev_servers_for_id(channel_id).len(), 1); - assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1"); - assert_eq!( - store.dev_servers_for_id(channel_id)[0].status, - DevServerStatus::Offline - ); + store.update(cx, |store, _| { + assert_eq!(store.dev_servers().len(), 1); + assert_eq!(store.dev_servers()[0].name, "server-1"); + assert_eq!(store.dev_servers()[0].status, DevServerStatus::Offline); }); let dev_server = server.create_dev_server(resp.access_token, cx2).await; cx.executor().run_until_parked(); - client.channel_store().update(cx, |store, _| { - assert_eq!( - store.dev_servers_for_id(channel_id)[0].status, - DevServerStatus::Online - ); + store.update(cx, |store, _| { + assert_eq!(store.dev_servers()[0].status, DevServerStatus::Online); }); dev_server @@ -54,13 +49,10 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC ) .await; - client - .channel_store() + store .update(cx, |store, cx| { store.create_remote_project( - channel_id, client::DevServerId(resp.dev_server_id), - "project-1".to_string(), "/remote".to_string(), cx, ) @@ -70,12 +62,11 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC cx.executor().run_until_parked(); - let remote_workspace = client - .channel_store() + let remote_workspace = store .update(cx, |store, cx| { - let projects = store.remote_projects_for_id(channel_id); + let projects = store.remote_projects(); assert_eq!(projects.len(), 1); - assert_eq!(projects[0].name, "project-1"); + assert_eq!(projects[0].path, "/remote"); workspace::join_remote_project( projects[0].project_id.unwrap(), client.app_state.clone(), @@ -87,19 +78,19 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC cx.executor().run_until_parked(); - let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut(); - cx2.simulate_keystrokes("cmd-p 1 enter"); + let cx = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut(); + cx.simulate_keystrokes("cmd-p 1 enter"); let editor = remote_workspace - .update(cx2, |ws, cx| { + .update(cx, |ws, cx| { ws.active_item_as::(cx).unwrap().clone() }) .unwrap(); - editor.update(cx2, |ed, cx| { + editor.update(cx, |ed, cx| { assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote"); }); - cx2.simulate_input("wow!"); - cx2.simulate_keystrokes("cmd-s"); + cx.simulate_input("wow!"); + cx.simulate_keystrokes("cmd-s"); let content = dev_server .fs() @@ -108,3 +99,263 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC .unwrap(); assert_eq!(content, "wow!remote\nremote\nremote\n"); } + +#[gpui::test] +async fn test_dev_server_env_files( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, +) { + let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await; + + let (_dev_server, remote_workspace) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await; + + cx1.executor().run_until_parked(); + + let cx1 = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut(); + cx1.simulate_keystrokes("cmd-p . e enter"); + + let editor = remote_workspace + .update(cx1, |ws, cx| { + ws.active_item_as::(cx).unwrap().clone() + }) + .unwrap(); + editor.update(cx1, |ed, cx| { + assert_eq!(ed.text(cx).to_string(), "SECRET"); + }); + + cx1.update(|cx| { + workspace::join_channel( + channel_id, + client1.app_state.clone(), + Some(remote_workspace), + cx, + ) + }) + .await + .unwrap(); + cx1.executor().run_until_parked(); + + remote_workspace + .update(cx1, |ws, cx| { + assert!(ws.project().read(cx).is_shared()); + }) + .unwrap(); + + join_channel(channel_id, &client2, cx2).await.unwrap(); + cx2.executor().run_until_parked(); + + let (workspace2, cx2) = client2.active_workspace(cx2); + let editor = workspace2.update(cx2, |ws, cx| { + ws.active_item_as::(cx).unwrap().clone() + }); + // TODO: it'd be nice to hide .env files from other people + editor.update(cx2, |ed, cx| { + assert_eq!(ed.text(cx).to_string(), "SECRET"); + }); +} + +async fn create_remote_project( + server: &TestServer, + client_app_state: Arc, + cx: &mut TestAppContext, + cx_devserver: &mut TestAppContext, +) -> (TestClient, WindowHandle) { + let store = cx.update(|cx| remote_projects::Store::global(cx).clone()); + + let resp = store + .update(cx, |store, cx| { + store.create_dev_server("server-1".to_string(), cx) + }) + .await + .unwrap(); + let dev_server = server + .create_dev_server(resp.access_token, cx_devserver) + .await; + + cx.executor().run_until_parked(); + + dev_server + .fs() + .insert_tree( + "/remote", + json!({ + "1.txt": "remote\nremote\nremote", + ".env": "SECRET", + }), + ) + .await; + + store + .update(cx, |store, cx| { + store.create_remote_project( + client::DevServerId(resp.dev_server_id), + "/remote".to_string(), + cx, + ) + }) + .await + .unwrap(); + + cx.executor().run_until_parked(); + + let workspace = store + .update(cx, |store, cx| { + let projects = store.remote_projects(); + assert_eq!(projects.len(), 1); + assert_eq!(projects[0].path, "/remote"); + workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx) + }) + .await + .unwrap(); + + cx.executor().run_until_parked(); + + (dev_server, workspace) +} + +#[gpui::test] +async fn test_dev_server_leave_room( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, +) { + let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await; + + let (_dev_server, remote_workspace) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await; + + cx1.update(|cx| { + workspace::join_channel( + channel_id, + client1.app_state.clone(), + Some(remote_workspace), + cx, + ) + }) + .await + .unwrap(); + cx1.executor().run_until_parked(); + + remote_workspace + .update(cx1, |ws, cx| { + assert!(ws.project().read(cx).is_shared()); + }) + .unwrap(); + + join_channel(channel_id, &client2, cx2).await.unwrap(); + cx2.executor().run_until_parked(); + + cx1.update(|cx| ActiveCall::global(cx).update(cx, |active_call, cx| active_call.hang_up(cx))) + .await + .unwrap(); + + cx1.executor().run_until_parked(); + + let (workspace, cx2) = client2.active_workspace(cx2); + cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected())); +} + +#[gpui::test] +async fn test_dev_server_reconnect( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, +) { + let (mut server, client1) = TestServer::start1(cx1).await; + let channel_id = server + .make_channel("test", None, (&client1, cx1), &mut []) + .await; + + let (_dev_server, remote_workspace) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx3).await; + + cx1.update(|cx| { + workspace::join_channel( + channel_id, + client1.app_state.clone(), + Some(remote_workspace), + cx, + ) + }) + .await + .unwrap(); + cx1.executor().run_until_parked(); + + remote_workspace + .update(cx1, |ws, cx| { + assert!(ws.project().read(cx).is_shared()); + }) + .unwrap(); + + drop(client1); + + let client2 = server.create_client(cx2, "user_a").await; + + let store = cx2.update(|cx| remote_projects::Store::global(cx).clone()); + + store + .update(cx2, |store, cx| { + let projects = store.remote_projects(); + workspace::join_remote_project( + projects[0].project_id.unwrap(), + client2.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); +} + +#[gpui::test] +async fn test_create_remote_project_path_validation( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, +) { + let (server, client1) = TestServer::start1(cx1).await; + let _channel_id = server + .make_channel("test", None, (&client1, cx1), &mut []) + .await; + + // Creating a project with a path that does exist should not fail + let (_dev_server, _) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await; + + cx1.executor().run_until_parked(); + + let store = cx1.update(|cx| remote_projects::Store::global(cx).clone()); + + let resp = store + .update(cx1, |store, cx| { + store.create_dev_server("server-2".to_string(), cx) + }) + .await + .unwrap(); + + cx1.executor().run_until_parked(); + + let _dev_server = server.create_dev_server(resp.access_token, cx3).await; + + cx1.executor().run_until_parked(); + + // Creating a remote project with a path that does not exist should fail + let result = store + .update(cx1, |store, cx| { + store.create_remote_project( + client::DevServerId(resp.dev_server_id), + "/notfound".to_string(), + cx, + ) + }) + .await; + + cx1.executor().run_until_parked(); + + let error = result.unwrap_err(); + assert!(matches!( + error.error_code(), + ErrorCode::RemoteProjectPathDoesNotExist + )); +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 728c8806fe..65e57a8ff3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3743,6 +3743,10 @@ async fn test_leaving_project( buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + // Drop client B's connection and ensure client A and client C observe client B leaving. client_b.disconnect(&cx_b.to_async()); executor.advance_clock(RECONNECT_TIMEOUT); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index fc0d0fdaf9..f189fd22db 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -284,6 +284,7 @@ impl TestServer { collab_ui::init(&app_state, cx); file_finder::init(cx); menu::init(); + remote_projects::init(client.clone(), cx); settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap(); }); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 89d669f991..ff78b50853 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -39,7 +39,6 @@ db.workspace = true editor.workspace = true emojis.workspace = true extensions_ui.workspace = true -feature_flags.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 49753ccd6f..59099dd486 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -305,10 +305,6 @@ impl ChannelView { }); } ChannelBufferEvent::BufferEdited => { - // Emit the edited event on the editor context so that other views can update it's state (e.g. markdown preview) - self.editor.update(cx, |_, cx| { - cx.emit(EditorEvent::Edited); - }); if self.editor.read(cx).is_focused(cx) { self.acknowledge_buffer_version(cx); } else { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index b27b891c38..8b5eed08d9 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1,20 +1,17 @@ mod channel_modal; mod contact_finder; -mod dev_server_modal; use self::channel_modal::ChannelModal; -use self::dev_server_modal::DevServerModal; use crate::{ channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile, CollaborationPanelSettings, }; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject}; +use channel::{Channel, ChannelEvent, ChannelStore}; use client::{ChannelId, Client, Contact, ProjectId, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; -use feature_flags::{self, FeatureFlagAppExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement, @@ -27,7 +24,7 @@ use gpui::{ use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev}; use project::{Fs, Project}; use rpc::{ - proto::{self, ChannelVisibility, DevServerStatus, PeerId}, + proto::{self, ChannelVisibility, PeerId}, ErrorCode, ErrorExt, }; use serde_derive::{Deserialize, Serialize}; @@ -191,7 +188,6 @@ enum ListEntry { id: ProjectId, name: SharedString, }, - RemoteProject(channel::RemoteProject), Contact { contact: Arc, calling: bool, @@ -282,23 +278,10 @@ impl CollabPanel { .push(cx.observe(&this.user_store, |this, _, cx| { this.update_entries(true, cx) })); - let mut has_opened = false; - this.subscriptions.push(cx.observe( - &this.channel_store, - move |this, channel_store, cx| { - if !has_opened { - if !channel_store - .read(cx) - .dev_servers_for_id(ChannelId(1)) - .is_empty() - { - this.manage_remote_projects(ChannelId(1), cx); - has_opened = true; - } - } + this.subscriptions + .push(cx.observe(&this.channel_store, move |this, _, cx| { this.update_entries(true, cx) - }, - )); + })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); this.subscriptions.push(cx.subscribe( @@ -586,7 +569,6 @@ impl CollabPanel { } let hosted_projects = channel_store.projects_for_id(channel.id); - let remote_projects = channel_store.remote_projects_for_id(channel.id); let has_children = channel_store .channel_at_index(mat.candidate_id + 1) .map_or(false, |next_channel| { @@ -624,12 +606,6 @@ impl CollabPanel { for (name, id) in hosted_projects { self.entries.push(ListEntry::HostedProject { id, name }); } - - if cx.has_flag::() { - for remote_project in remote_projects { - self.entries.push(ListEntry::RemoteProject(remote_project)); - } - } } } @@ -1089,59 +1065,6 @@ impl CollabPanel { .tooltip(move |cx| Tooltip::text("Open Project", cx)) } - fn render_remote_project( - &self, - remote_project: &RemoteProject, - is_selected: bool, - cx: &mut ViewContext, - ) -> impl IntoElement { - let id = remote_project.id; - let name = remote_project.name.clone(); - let maybe_project_id = remote_project.project_id; - - let dev_server = self - .channel_store - .read(cx) - .find_dev_server_by_id(remote_project.dev_server_id); - - let tooltip_text = SharedString::from(match dev_server { - Some(dev_server) => format!("Open Remote Project ({})", dev_server.name), - None => "Open Remote Project".to_string(), - }); - - let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online); - - let dev_server_text_color = if dev_server_is_online { - Color::Default - } else { - Color::Disabled - }; - - ListItem::new(ElementId::NamedInteger( - "remote-project".into(), - id.0 as usize, - )) - .indent_level(2) - .indent_step_size(px(20.)) - .selected(is_selected) - .on_click(cx.listener(move |this, _, cx| { - //TODO display error message if dev server is offline - if dev_server_is_online { - if let Some(project_id) = maybe_project_id { - this.join_remote_project(project_id, cx); - } - } - })) - .start_slot( - h_flex() - .relative() - .gap_1() - .child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)), - ) - .child(Label::new(name.clone()).color(dev_server_text_color)) - .tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)) - } - fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1343,24 +1266,11 @@ impl CollabPanel { } if self.channel_store.read(cx).is_root_channel(channel_id) { - context_menu = context_menu - .separator() - .entry( - "Manage Members", - None, - cx.handler_for(&this, move |this, cx| { - this.manage_members(channel_id, cx) - }), - ) - .when(cx.has_flag::(), |context_menu| { - context_menu.entry( - "Manage Remote Projects", - None, - cx.handler_for(&this, move |this, cx| { - this.manage_remote_projects(channel_id, cx) - }), - ) - }) + context_menu = context_menu.separator().entry( + "Manage Members", + None, + cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), + ) } else { context_menu = context_menu.entry( "Move this channel", @@ -1624,12 +1534,6 @@ impl CollabPanel { } => { // todo() } - ListEntry::RemoteProject(project) => { - if let Some(project_id) = project.project_id { - self.join_remote_project(project_id, cx) - } - } - ListEntry::OutgoingRequest(_) => {} ListEntry::ChannelEditor { .. } => {} } @@ -1801,18 +1705,6 @@ impl CollabPanel { self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); } - fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { - let channel_store = self.channel_store.clone(); - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| { - DevServerModal::new(channel_store.clone(), channel_id, cx) - }); - }); - } - fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) @@ -2113,18 +2005,6 @@ impl CollabPanel { .detach_and_prompt_err("Failed to join channel", cx, |_, _| None) } - fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err( - "Failed to join project", - cx, - |_, _| None, - ) - } - fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; @@ -2260,9 +2140,6 @@ impl CollabPanel { ListEntry::HostedProject { id, name } => self .render_channel_project(*id, name, is_selected, cx) .into_any_element(), - ListEntry::RemoteProject(remote_project) => self - .render_remote_project(remote_project, is_selected, cx) - .into_any_element(), } } @@ -3005,11 +2882,6 @@ impl PartialEq for ListEntry { return id == other_id; } } - ListEntry::RemoteProject(project) => { - if let ListEntry::RemoteProject(other) = other { - return project.id == other.id; - } - } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, diff --git a/crates/collab_ui/src/collab_panel/dev_server_modal.rs b/crates/collab_ui/src/collab_panel/dev_server_modal.rs deleted file mode 100644 index 4e2057c140..0000000000 --- a/crates/collab_ui/src/collab_panel/dev_server_modal.rs +++ /dev/null @@ -1,622 +0,0 @@ -use channel::{ChannelStore, DevServer, RemoteProject}; -use client::{ChannelId, DevServerId, RemoteProjectId}; -use editor::Editor; -use gpui::{ - AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, - ScrollHandle, Task, View, ViewContext, -}; -use rpc::proto::{self, CreateDevServerResponse, DevServerStatus}; -use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip}; -use util::ResultExt; -use workspace::ModalView; - -pub struct DevServerModal { - mode: Mode, - focus_handle: FocusHandle, - scroll_handle: ScrollHandle, - channel_store: Model, - channel_id: ChannelId, - remote_project_name_editor: View, - remote_project_path_editor: View, - dev_server_name_editor: View, - _subscriptions: [gpui::Subscription; 2], -} - -#[derive(Default)] -struct CreateDevServer { - creating: Option>, - dev_server: Option, -} - -struct CreateRemoteProject { - dev_server_id: DevServerId, - creating: Option>, - remote_project: Option, -} - -enum Mode { - Default, - CreateRemoteProject(CreateRemoteProject), - CreateDevServer(CreateDevServer), -} - -impl DevServerModal { - pub fn new( - channel_store: Model, - channel_id: ChannelId, - cx: &mut ViewContext, - ) -> Self { - let name_editor = cx.new_view(|cx| Editor::single_line(cx)); - let path_editor = cx.new_view(|cx| Editor::single_line(cx)); - let dev_server_name_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Dev server name", cx); - editor - }); - - let focus_handle = cx.focus_handle(); - - let subscriptions = [ - cx.observe(&channel_store, |_, _, cx| { - cx.notify(); - }), - cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }), - ]; - - Self { - mode: Mode::Default, - focus_handle, - scroll_handle: ScrollHandle::new(), - channel_store, - channel_id, - remote_project_name_editor: name_editor, - remote_project_path_editor: path_editor, - dev_server_name_editor, - _subscriptions: subscriptions, - } - } - - pub fn create_remote_project( - &mut self, - dev_server_id: DevServerId, - cx: &mut ViewContext, - ) { - let channel_id = self.channel_id; - let name = self - .remote_project_name_editor - .read(cx) - .text(cx) - .trim() - .to_string(); - let path = self - .remote_project_path_editor - .read(cx) - .text(cx) - .trim() - .to_string(); - - if name == "" { - return; - } - if path == "" { - return; - } - - let create = self.channel_store.update(cx, |store, cx| { - store.create_remote_project(channel_id, dev_server_id, name, path, cx) - }); - - let task = cx.spawn(|this, mut cx| async move { - let result = create.await; - if let Err(e) = &result { - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to create project", - Some(&format!("{:?}. Please try again.", e)), - &["Ok"], - ) - .await - .log_err(); - } - this.update(&mut cx, |this, _| { - this.mode = Mode::CreateRemoteProject(CreateRemoteProject { - dev_server_id, - creating: None, - remote_project: result.ok().and_then(|r| r.remote_project), - }); - }) - .log_err(); - }); - - self.mode = Mode::CreateRemoteProject(CreateRemoteProject { - dev_server_id, - creating: Some(task), - remote_project: None, - }); - } - - pub fn create_dev_server(&mut self, cx: &mut ViewContext) { - let name = self - .dev_server_name_editor - .read(cx) - .text(cx) - .trim() - .to_string(); - - if name == "" { - return; - } - - let dev_server = self.channel_store.update(cx, |store, cx| { - store.create_dev_server(self.channel_id, name.clone(), cx) - }); - - let task = cx.spawn(|this, mut cx| async move { - match dev_server.await { - Ok(dev_server) => { - this.update(&mut cx, |this, _| { - this.mode = Mode::CreateDevServer(CreateDevServer { - creating: None, - dev_server: Some(dev_server), - }); - }) - .log_err(); - } - Err(e) => { - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to create server", - Some(&format!("{:?}. Please try again.", e)), - &["Ok"], - ) - .await - .log_err(); - this.update(&mut cx, |this, _| { - this.mode = Mode::CreateDevServer(Default::default()); - }) - .log_err(); - } - } - }); - - self.mode = Mode::CreateDevServer(CreateDevServer { - creating: Some(task), - dev_server: None, - }); - cx.notify() - } - - fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - match self.mode { - Mode::Default => cx.emit(DismissEvent), - Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => { - self.mode = Mode::Default; - cx.notify(); - } - } - } - - fn render_dev_server( - &mut self, - dev_server: &DevServer, - cx: &mut ViewContext, - ) -> impl IntoElement { - let channel_store = self.channel_store.read(cx); - let dev_server_id = dev_server.id; - let status = dev_server.status; - - v_flex() - .w_full() - .child( - h_flex() - .group("dev-server") - .justify_between() - .child( - h_flex() - .gap_2() - .child( - div() - .id(("status", dev_server.id.0)) - .relative() - .child(Icon::new(IconName::Server).size(IconSize::Small)) - .child( - div().absolute().bottom_0().left(rems_from_px(8.0)).child( - Indicator::dot().color(match status { - DevServerStatus::Online => Color::Created, - DevServerStatus::Offline => Color::Deleted, - }), - ), - ) - .tooltip(move |cx| { - Tooltip::text( - match status { - DevServerStatus::Online => "Online", - DevServerStatus::Offline => "Offline", - }, - cx, - ) - }), - ) - .child(dev_server.name.clone()) - .child( - h_flex() - .visible_on_hover("dev-server") - .gap_1() - .child( - IconButton::new("edit-dev-server", IconName::Pencil) - .disabled(true) //TODO implement this on the collab side - .tooltip(|cx| { - Tooltip::text("Coming Soon - Edit dev server", cx) - }), - ) - .child( - IconButton::new("remove-dev-server", IconName::Trash) - .disabled(true) //TODO implement this on the collab side - .tooltip(|cx| { - Tooltip::text("Coming Soon - Remove dev server", cx) - }), - ), - ), - ) - .child( - h_flex().gap_1().child( - IconButton::new("add-remote-project", IconName::Plus) - .tooltip(|cx| Tooltip::text("Add a remote project", cx)) - .on_click(cx.listener(move |this, _, cx| { - this.mode = Mode::CreateRemoteProject(CreateRemoteProject { - dev_server_id, - creating: None, - remote_project: None, - }); - cx.notify(); - })), - ), - ), - ) - .child( - v_flex() - .w_full() - .bg(cx.theme().colors().title_bar_background) - .border() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .my_1() - .py_0p5() - .px_3() - .child( - List::new().empty_message("No projects.").children( - channel_store - .remote_projects_for_id(dev_server.channel_id) - .iter() - .filter_map(|remote_project| { - if remote_project.dev_server_id == dev_server.id { - Some(self.render_remote_project(remote_project, cx)) - } else { - None - } - }), - ), - ), - ) - // .child(div().ml_8().child( - // Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener( - // move |this, _, cx| { - // this.mode = Mode::CreateRemoteProject(CreateRemoteProject { - // dev_server_id, - // creating: None, - // remote_project: None, - // }); - // cx.notify(); - // }, - // )), - // )) - } - - fn render_remote_project( - &mut self, - project: &RemoteProject, - _: &mut ViewContext, - ) -> impl IntoElement { - h_flex() - .gap_2() - .child(Icon::new(IconName::FileTree)) - .child(Label::new(project.name.clone())) - .child(Label::new(format!("({})", project.path.clone())).color(Color::Muted)) - } - - fn render_create_dev_server(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let Mode::CreateDevServer(CreateDevServer { - creating, - dev_server, - }) = &self.mode - else { - unreachable!() - }; - - self.dev_server_name_editor.update(cx, |editor, _| { - editor.set_read_only(creating.is_some() || dev_server.is_some()) - }); - v_flex() - .px_1() - .pt_0p5() - .gap_px() - .child( - v_flex().py_0p5().px_1().child( - h_flex() - .px_1() - .py_0p5() - .child( - IconButton::new("back", IconName::ArrowLeft) - .style(ButtonStyle::Transparent) - .on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| { - this.mode = Mode::Default; - cx.notify(); - })), - ) - .child(Headline::new("Register dev server")), - ), - ) - .child( - h_flex() - .ml_5() - .gap_2() - .child("Name") - .child(self.dev_server_name_editor.clone()) - .on_action( - cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)), - ) - .when(creating.is_none() && dev_server.is_none(), |div| { - div.child( - Button::new("create-dev-server", "Create").on_click(cx.listener( - move |this, _, cx| { - this.create_dev_server(cx); - }, - )), - ) - }) - .when(creating.is_some() && dev_server.is_none(), |div| { - div.child(Button::new("create-dev-server", "Creating...").disabled(true)) - }), - ) - .when_some(dev_server.clone(), |div, dev_server| { - let channel_store = self.channel_store.read(cx); - let status = channel_store - .find_dev_server_by_id(DevServerId(dev_server.dev_server_id)) - .map(|server| server.status) - .unwrap_or(DevServerStatus::Offline); - let instructions = SharedString::from(format!( - "zed --dev-server-token {}", - dev_server.access_token - )); - div.child( - v_flex() - .ml_8() - .gap_2() - .child(Label::new(format!( - "Please log into `{}` and run:", - dev_server.name - ))) - .child(instructions.clone()) - .child( - IconButton::new("copy-access-token", IconName::Copy) - .on_click(cx.listener(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new( - instructions.to_string(), - )) - })) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::text("Copy access token", cx)), - ) - .when(status == DevServerStatus::Offline, |this| { - this.child(Label::new("Waiting for connection...")) - }) - .when(status == DevServerStatus::Online, |this| { - this.child(Label::new("Connection established! 🎊")).child( - Button::new("done", "Done").on_click(cx.listener(|this, _, cx| { - this.mode = Mode::Default; - cx.notify(); - })), - ) - }), - ) - }) - } - - fn render_default(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let channel_store = self.channel_store.read(cx); - let dev_servers = channel_store.dev_servers_for_id(self.channel_id); - // let dev_servers = Vec::new(); - - v_flex() - .id("scroll-container") - .h_full() - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .px_1() - .pt_0p5() - .gap_px() - .child( - ModalHeader::new("Manage Remote Project") - .child(Headline::new("Remote Projects").size(HeadlineSize::Small)), - ) - .child( - ModalContent::new().child( - List::new() - .empty_message("No dev servers registered.") - .header(Some( - ListHeader::new("Dev Servers").end_slot( - Button::new("register-dev-server-button", "New Server") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .tooltip(|cx| Tooltip::text("Register a new dev server", cx)) - .on_click(cx.listener(|this, _, cx| { - this.mode = Mode::CreateDevServer(Default::default()); - this.dev_server_name_editor - .read(cx) - .focus_handle(cx) - .focus(cx); - cx.notify(); - })), - ), - )) - .children(dev_servers.iter().map(|dev_server| { - self.render_dev_server(dev_server, cx).into_any_element() - })), - ), - ) - } - - fn render_create_project(&self, cx: &mut ViewContext) -> impl IntoElement { - let Mode::CreateRemoteProject(CreateRemoteProject { - dev_server_id, - creating, - remote_project, - }) = &self.mode - else { - unreachable!() - }; - let channel_store = self.channel_store.read(cx); - let (dev_server_name, dev_server_status) = channel_store - .find_dev_server_by_id(*dev_server_id) - .map(|server| (server.name.clone(), server.status)) - .unwrap_or((SharedString::from(""), DevServerStatus::Offline)); - v_flex() - .px_1() - .pt_0p5() - .gap_px() - .child( - ModalHeader::new("Manage Remote Project") - .child(Headline::new("Manage Remote Projects")), - ) - .child( - h_flex() - .py_0p5() - .px_1() - .child(div().px_1().py_0p5().child( - IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener( - |this, _, cx| { - this.mode = Mode::Default; - cx.notify() - }, - )), - )) - .child("Add Project..."), - ) - .child( - h_flex() - .ml_5() - .gap_2() - .child( - div() - .id(("status", dev_server_id.0)) - .relative() - .child(Icon::new(IconName::Server)) - .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child( - Indicator::dot().color(match dev_server_status { - DevServerStatus::Online => Color::Created, - DevServerStatus::Offline => Color::Deleted, - }), - )) - .tooltip(move |cx| { - Tooltip::text( - match dev_server_status { - DevServerStatus::Online => "Online", - DevServerStatus::Offline => "Offline", - }, - cx, - ) - }), - ) - .child(dev_server_name.clone()), - ) - .child( - h_flex() - .ml_5() - .gap_2() - .child("Name") - .child(self.remote_project_name_editor.clone()) - .on_action(cx.listener(|this, _: &menu::Confirm, cx| { - cx.focus_view(&this.remote_project_path_editor) - })), - ) - .child( - h_flex() - .ml_5() - .gap_2() - .child("Path") - .child(self.remote_project_path_editor.clone()) - .on_action( - cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)), - ) - .when(creating.is_none() && remote_project.is_none(), |div| { - div.child(Button::new("create-remote-server", "Create").on_click({ - let dev_server_id = *dev_server_id; - cx.listener(move |this, _, cx| { - this.create_remote_project(dev_server_id, cx) - }) - })) - }) - .when(creating.is_some(), |div| { - div.child(Button::new("create-dev-server", "Creating...").disabled(true)) - }), - ) - .when_some(remote_project.clone(), |div, remote_project| { - let channel_store = self.channel_store.read(cx); - let status = channel_store - .find_remote_project_by_id(RemoteProjectId(remote_project.id)) - .map(|project| { - if project.project_id.is_some() { - DevServerStatus::Online - } else { - DevServerStatus::Offline - } - }) - .unwrap_or(DevServerStatus::Offline); - div.child( - v_flex() - .ml_5() - .ml_8() - .gap_2() - .when(status == DevServerStatus::Offline, |this| { - this.child(Label::new("Waiting for project...")) - }) - .when(status == DevServerStatus::Online, |this| { - this.child(Label::new("Project online! 🎊")).child( - Button::new("done", "Done").on_click(cx.listener(|this, _, cx| { - this.mode = Mode::Default; - cx.notify(); - })), - ) - }), - ) - }) - } -} -impl ModalView for DevServerModal {} - -impl FocusableView for DevServerModal { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for DevServerModal {} - -impl Render for DevServerModal { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .track_focus(&self.focus_handle) - .elevation_3(cx) - .key_context("DevServerModal") - .on_action(cx.listener(Self::cancel)) - .pb_4() - .w(rems(34.)) - .min_h(rems(20.)) - .max_h(rems(40.)) - .child(match &self.mode { - Mode::Default => self.render_default(cx).into_any_element(), - Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(), - Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(), - }) - } -} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 7fd8b26b72..5f9ee3a013 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -171,44 +171,48 @@ impl Render for CollabTitlebarItem { let room = room.read(cx); let project = self.project.read(cx); let is_local = project.is_local(); - let is_shared = is_local && project.is_shared(); + let is_remote_project = project.remote_project_id().is_some(); + let is_shared = (is_local || is_remote_project) && project.is_shared(); 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_share_projects = room.can_share_projects(); - this.when(is_local && can_share_projects, |this| { - this.child( - Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - ) - .tooltip(move |cx| { - Tooltip::text( - if is_shared { - "Stop sharing project with call participants" - } else { - "Share project with call participants" - }, - cx, + this.when( + (is_local || is_remote_project) && can_share_projects, + |this| { + this.child( + Button::new( + "toggle_sharing", + if is_shared { "Unshare" } else { "Share" }, ) - }) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .selected(is_shared) - .label_size(LabelSize::Small) - .on_click(cx.listener( - move |this, _, cx| { - if is_shared { - this.unshare_project(&Default::default(), cx); - } else { - this.share_project(&Default::default(), cx); - } - }, - )), - ) - }) + .tooltip(move |cx| { + Tooltip::text( + if is_shared { + "Stop sharing project with call participants" + } else { + "Share project with call participants" + }, + cx, + ) + }) + .style(ButtonStyle::Subtle) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(is_shared) + .label_size(LabelSize::Small) + .on_click(cx.listener( + move |this, _, cx| { + if is_shared { + this.unshare_project(&Default::default(), cx); + } else { + this.share_project(&Default::default(), cx); + } + }, + )), + ) + }, + ) .child( div() .child( @@ -406,7 +410,7 @@ impl CollabTitlebarItem { ) } - pub fn render_project_name(&self, cx: &mut ViewContext) -> impl Element { + pub fn render_project_name(&self, cx: &mut ViewContext) -> impl IntoElement { let name = { let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| { let worktree = worktree.read(cx); @@ -423,15 +427,26 @@ impl CollabTitlebarItem { }; let workspace = self.workspace.clone(); - popover_menu("project_name_trigger") - .trigger( - Button::new("project_name_trigger", name) - .when(!is_project_selected, |b| b.color(Color::Muted)) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), - ) - .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx))) + Button::new("project_name_trigger", name) + .when(!is_project_selected, |b| b.color(Color::Muted)) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .tooltip(move |cx| { + Tooltip::for_action( + "Recent Projects", + &recent_projects::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); + }) + } + })) } pub fn render_project_branch(&self, cx: &mut ViewContext) -> Option { @@ -607,17 +622,6 @@ impl CollabTitlebarItem { Some(view) } - pub fn render_project_popover( - workspace: WeakView, - cx: &mut WindowContext<'_>, - ) -> View { - let view = RecentProjects::open_popover(workspace, cx); - - let focus_handle = view.focus_handle(cx); - cx.focus(&focus_handle); - view - } - fn render_connection_status( &self, status: &client::Status, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ac3b3e95c1..da617c3fea 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -81,6 +81,7 @@ impl FollowableItem for Editor { let mut buffers = futures::future::try_join_all(buffers?) .await .debug_assert_ok("leaders don't share views for unshared buffers")?; + let editor = pane.update(&mut cx, |pane, cx| { let mut editors = pane.items_of_type::(); editors.find(|editor| { diff --git a/crates/headless/src/headless.rs b/crates/headless/src/headless.rs index 677389b637..13e6cbb9fa 100644 --- a/crates/headless/src/headless.rs +++ b/crates/headless/src/headless.rs @@ -1,20 +1,25 @@ use anyhow::Result; -use client::{user::UserStore, Client, ClientSettings, RemoteProjectId}; +use client::RemoteProjectId; +use client::{user::UserStore, Client, ClientSettings}; use fs::Fs; use futures::Future; -use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel}; +use gpui::{ + AppContext, AsyncAppContext, BorrowAppContext, Context, Global, Model, ModelContext, Task, + WeakModel, +}; use language::LanguageRegistry; use node_runtime::NodeRuntime; use postage::stream::Stream; -use project::Project; -use rpc::{proto, TypedEnvelope}; -use settings::Settings; +use project::{Project, WorktreeSettings}; +use rpc::{proto, ErrorCode, TypedEnvelope}; +use settings::{Settings, SettingsStore}; use std::{collections::HashMap, sync::Arc}; use util::{ResultExt, TryFutureExt}; pub struct DevServer { client: Arc, app_state: AppState, + remote_shutdown: bool, projects: HashMap>, _subscriptions: Vec, _maintain_connection: Task>, @@ -35,6 +40,15 @@ pub fn init(client: Arc, app_state: AppState, cx: &mut AppContext) { let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx)); cx.set_global(GlobalDevServer(dev_server.clone())); + // Dev server cannot have any private files for now + cx.update_global(|store: &mut SettingsStore, _| { + let old_settings = store.get::(None); + store.override_global(WorktreeSettings { + private_files: Some(vec![]), + ..old_settings.clone() + }); + }); + // Set up a handler when the dev server is shut down by the user pressing Ctrl-C let (tx, rx) = futures::channel::oneshot::channel(); set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err(); @@ -53,7 +67,7 @@ pub fn init(client: Arc, app_state: AppState, cx: &mut AppContext) { log::info!("Connected to {}", server_url); } Err(e) => { - log::error!("Error connecting to {}: {}", server_url, e); + log::error!("Error connecting to '{}': {}", server_url, e); cx.update(|cx| cx.quit()).log_err(); } } @@ -89,19 +103,31 @@ impl DevServer { DevServer { _subscriptions: vec![ - client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions) + client.add_message_handler(cx.weak_model(), Self::handle_dev_server_instructions), + client.add_request_handler( + cx.weak_model(), + Self::handle_validate_remote_project_request, + ), + client.add_message_handler(cx.weak_model(), Self::handle_shutdown), ], _maintain_connection: maintain_connection, projects: Default::default(), + remote_shutdown: false, app_state, client, } } fn app_will_quit(&mut self, _: &mut ModelContext) -> impl Future { - let request = self.client.request(proto::ShutdownDevServer {}); + let request = if self.remote_shutdown { + None + } else { + Some(self.client.request(proto::ShutdownDevServer {})) + }; async move { - request.await.log_err(); + if let Some(request) = request { + request.await.log_err(); + } } } @@ -148,6 +174,35 @@ impl DevServer { Ok(()) } + async fn handle_validate_remote_project_request( + this: Model, + envelope: TypedEnvelope, + _: Arc, + cx: AsyncAppContext, + ) -> Result { + let path = std::path::Path::new(&envelope.payload.path); + let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?; + + let path_exists = fs.is_dir(path).await; + if !path_exists { + return Err(anyhow::anyhow!(ErrorCode::RemoteProjectPathDoesNotExist))?; + } + + Ok(proto::Ack {}) + } + + async fn handle_shutdown( + this: Model, + _envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.remote_shutdown = true; + cx.quit(); + }) + } + fn unshare_project( &mut self, remote_project_id: &RemoteProjectId, diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs index 02994c87a7..9d49368f71 100644 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -11,6 +11,7 @@ pub struct HighlightedText { pub text: String, pub highlight_positions: Vec, pub char_count: usize, + pub color: Color, } impl HighlightedText { @@ -39,13 +40,17 @@ impl HighlightedText { text, highlight_positions, char_count, + color: Color::Default, } } -} + pub fn color(self, color: Color) -> Self { + Self { color, ..self } + } +} impl RenderOnce for HighlightedText { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions) + fn render(self, _: &mut WindowContext) -> impl IntoElement { + HighlightedLabel::new(self.text, self.highlight_positions).color(self.color) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6113ea8e92..ad7b67ffe0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,7 +15,8 @@ pub mod search_history; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use client::{ - proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore, + proto, Client, Collaborator, PendingEntitySubscription, ProjectId, RemoteProjectId, + TypedEnvelope, UserStore, }; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; @@ -207,6 +208,7 @@ pub struct Project { prettier_instances: HashMap, tasks: Model, hosted_project_id: Option, + remote_project_id: Option, search_history: SearchHistory, } @@ -268,6 +270,7 @@ enum ProjectClientState { capability: Capability, remote_id: u64, replica_id: ReplicaId, + in_room: bool, }, } @@ -723,6 +726,7 @@ impl Project { prettier_instances: HashMap::default(), tasks, hosted_project_id: None, + remote_project_id: None, search_history: Self::new_search_history(), } }) @@ -836,6 +840,7 @@ impl Project { capability: Capability::ReadWrite, remote_id, replica_id, + in_room: response.payload.remote_project_id.is_none(), }, supplementary_language_servers: HashMap::default(), language_servers: Default::default(), @@ -877,6 +882,10 @@ impl Project { prettier_instances: HashMap::default(), tasks, hosted_project_id: None, + remote_project_id: response + .payload + .remote_project_id + .map(|remote_project_id| RemoteProjectId(remote_project_id)), search_history: Self::new_search_history(), }; this.set_role(role, cx); @@ -1235,6 +1244,10 @@ impl Project { self.hosted_project_id } + pub fn remote_project_id(&self) -> Option { + self.remote_project_id + } + pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, @@ -1552,7 +1565,16 @@ impl Project { pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext) -> Result<()> { if !matches!(self.client_state, ProjectClientState::Local) { - return Err(anyhow!("project was already shared")); + if let ProjectClientState::Remote { in_room, .. } = &mut self.client_state { + if *in_room || self.remote_project_id.is_none() { + return Err(anyhow!("project was already shared")); + } else { + *in_room = true; + return Ok(()); + } + } else { + return Err(anyhow!("project was already shared")); + } } self.client_subscriptions.push( self.client @@ -1763,7 +1785,14 @@ impl Project { fn unshare_internal(&mut self, cx: &mut AppContext) -> Result<()> { if self.is_remote() { - return Err(anyhow!("attempted to unshare a remote project")); + if self.remote_project_id().is_some() { + if let ProjectClientState::Remote { in_room, .. } = &mut self.client_state { + *in_room = false + } + return Ok(()); + } else { + return Err(anyhow!("attempted to unshare a remote project")); + } } if let ProjectClientState::Shared { remote_id, .. } = self.client_state { @@ -6959,7 +6988,8 @@ impl Project { pub fn is_shared(&self) -> bool { match &self.client_state { ProjectClientState::Shared { .. } => true, - ProjectClientState::Local | ProjectClientState::Remote { .. } => false, + ProjectClientState::Local => false, + ProjectClientState::Remote { in_room, .. } => *in_room, } } diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 3e8f63d133..e11ff9148e 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -13,14 +13,21 @@ path = "src/recent_projects.rs" doctest = false [dependencies] +anyhow.workspace = true +feature_flags.workspace = true fuzzy.workspace = true gpui.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true +remote_projects.workspace = true +rpc.workspace = true serde.workspace = true +settings.workspace = true smol.workspace = true +theme.workspace = true ui.workspace = true +ui_text_field.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index ba85a966bc..6090590b17 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,6 +1,9 @@ +mod remote_projects; + +use feature_flags::FeatureFlagAppExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, + Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Subscription, Task, View, ViewContext, WeakView, }; use ordered_float::OrderedFloat; @@ -8,11 +11,21 @@ use picker::{ highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, Picker, PickerDelegate, }; +use remote_projects::RemoteProjects; +use rpc::proto::DevServerStatus; use serde::Deserialize; -use std::{path::Path, sync::Arc}; -use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip}; -use util::paths::PathExt; -use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use ui::{ + prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem, + ListItemSpacing, Tooltip, +}; +use util::{paths::PathExt, ResultExt}; +use workspace::{ + AppState, ModalView, SerializedWorkspaceLocation, Workspace, WorkspaceId, WORKSPACE_DB, +}; #[derive(PartialEq, Clone, Deserialize, Default)] pub struct OpenRecent { @@ -25,9 +38,12 @@ fn default_create_new_window() -> bool { } gpui::impl_actions!(projects, [OpenRecent]); +gpui::actions!(projects, [OpenRemote]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); + cx.observe_new_views(remote_projects::RemoteProjects::register) + .detach(); } pub struct RecentProjects { @@ -55,10 +71,11 @@ impl RecentProjects { let workspaces = WORKSPACE_DB .recent_workspaces_on_disk() .await + .log_err() .unwrap_or_default(); this.update(&mut cx, move |this, cx| { this.picker.update(cx, move |picker, cx| { - picker.delegate.workspaces = workspaces; + picker.delegate.set_workspaces(workspaces); picker.update_matches(picker.query(cx), cx) }) }) @@ -75,9 +92,7 @@ impl RecentProjects { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, open_recent: &OpenRecent, cx| { let Some(recent_projects) = workspace.active_modal::(cx) else { - if let Some(handler) = Self::open(workspace, open_recent.create_new_window, cx) { - handler.detach_and_log_err(cx); - } + Self::open(workspace, open_recent.create_new_window, cx); return; }; @@ -89,24 +104,17 @@ impl RecentProjects { }); } - fn open( - _: &mut Workspace, + pub fn open( + workspace: &mut Workspace, create_new_window: bool, cx: &mut ViewContext, - ) -> Option>> { - Some(cx.spawn(|workspace, mut cx| async move { - workspace.update(&mut cx, |workspace, cx| { - let weak_workspace = cx.view().downgrade(); - workspace.toggle_modal(cx, |cx| { - let delegate = - RecentProjectsDelegate::new(weak_workspace, create_new_window, true); - - let modal = Self::new(delegate, 34., cx); - modal - }); - })?; - Ok(()) - })) + ) { + let weak = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = RecentProjectsDelegate::new(weak, create_new_window, true); + let modal = Self::new(delegate, 34., cx); + modal + }) } pub fn open_popover(workspace: WeakView, cx: &mut WindowContext<'_>) -> View { @@ -143,13 +151,14 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakView, - workspaces: Vec<(WorkspaceId, WorkspaceLocation)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, create_new_window: bool, // Flag to reset index when there is a new query vs not reset index when user delete an item reset_selected_match_index: bool, + has_any_remote_projects: bool, } impl RecentProjectsDelegate { @@ -162,8 +171,17 @@ impl RecentProjectsDelegate { create_new_window, render_paths, reset_selected_match_index: true, + has_any_remote_projects: false, } } + + pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { + self.workspaces = workspaces; + self.has_any_remote_projects = self + .workspaces + .iter() + .any(|(_, location)| matches!(location, SerializedWorkspaceLocation::Remote(_))); + } } impl EventEmitter for RecentProjectsDelegate {} impl PickerDelegate for RecentProjectsDelegate { @@ -210,12 +228,18 @@ impl PickerDelegate for RecentProjectsDelegate { .iter() .enumerate() .map(|(id, (_, location))| { - let combined_string = location - .paths() - .iter() - .map(|path| path.compact().to_string_lossy().into_owned()) - .collect::>() - .join(""); + let combined_string = match location { + SerializedWorkspaceLocation::Local(paths) => paths + .paths() + .iter() + .map(|path| path.compact().to_string_lossy().into_owned()) + .collect::>() + .join(""), + SerializedWorkspaceLocation::Remote(remote_project) => { + format!("{}{}", remote_project.dev_server_name, remote_project.path) + } + }; + StringMatchCandidate::new(id, combined_string) }) .collect::>(); @@ -261,30 +285,69 @@ impl PickerDelegate for RecentProjectsDelegate { if workspace.database_id() == *candidate_workspace_id { Task::ready(Ok(())) } else { - let candidate_paths = candidate_workspace_location.paths().as_ref().clone(); - if replace_current_window { - cx.spawn(move |workspace, mut cx| async move { - let continue_replacing = workspace - .update(&mut cx, |workspace, cx| { - workspace.prepare_to_close(true, cx) - })? - .await?; - if continue_replacing { - workspace - .update(&mut cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - candidate_paths, - cx, - ) - })? - .await + match candidate_workspace_location { + SerializedWorkspaceLocation::Local(paths) => { + let paths = paths.paths().as_ref().clone(); + if replace_current_window { + cx.spawn(move |workspace, mut cx| async move { + let continue_replacing = workspace + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + })? + .await?; + if continue_replacing { + workspace + .update(&mut cx, |workspace, cx| { + workspace + .open_workspace_for_paths(true, paths, cx) + })? + .await + } else { + Ok(()) + } + }) } else { - Ok(()) + workspace.open_workspace_for_paths(false, paths, cx) } - }) - } else { - workspace.open_workspace_for_paths(false, candidate_paths, cx) + } + //TODO support opening remote projects in the same window + SerializedWorkspaceLocation::Remote(remote_project) => { + let store = ::remote_projects::Store::global(cx).read(cx); + let Some(project_id) = store + .remote_project(remote_project.id) + .and_then(|p| p.project_id) + else { + let dev_server_name = remote_project.dev_server_name.clone(); + return cx.spawn(|workspace, mut cx| async move { + let response = + cx.prompt(gpui::PromptLevel::Warning, + "Dev Server is offline", + Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()), + &["Ok", "Open Settings"] + ).await?; + if response == 1 { + workspace.update(&mut cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| RemoteProjects::new(cx)) + })?; + } else { + workspace.update(&mut cx, |workspace, cx| { + RecentProjects::open(workspace, true, cx); + })?; + } + Ok(()) + }) + }; + if let Some(app_state) = AppState::global(cx).upgrade() { + let task = + workspace::join_remote_project(project_id, app_state, cx); + cx.spawn(|_, _| async move { + task.await?; + Ok(()) + }) + } else { + Task::ready(Err(anyhow::anyhow!("App state not found"))) + } + } } } }) @@ -295,6 +358,14 @@ impl PickerDelegate for RecentProjectsDelegate { fn dismissed(&mut self, _: &mut ViewContext>) {} + fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString { + if self.workspaces.is_empty() { + "Recently opened projects will show up here".into() + } else { + "No matches".into() + } + } + fn render_match( &self, ix: usize, @@ -308,9 +379,30 @@ impl PickerDelegate for RecentProjectsDelegate { let (workspace_id, location) = &self.workspaces[hit.candidate_id]; let is_current_workspace = self.is_current_workspace(*workspace_id, cx); + let is_remote = matches!(location, SerializedWorkspaceLocation::Remote(_)); + let dev_server_status = + if let SerializedWorkspaceLocation::Remote(remote_project) = location { + let store = ::remote_projects::Store::global(cx).read(cx); + Some( + store + .remote_project(remote_project.id) + .and_then(|p| store.dev_server(p.dev_server_id)) + .map(|s| s.status) + .unwrap_or_default(), + ) + } else { + None + }; + let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = location - .paths() + let paths = match location { + SerializedWorkspaceLocation::Local(paths) => paths.paths(), + SerializedWorkspaceLocation::Remote(remote_project) => Arc::new(vec![PathBuf::from( + format!("{}:{}", remote_project.dev_server_name, remote_project.path), + )]), + }; + + let (match_labels, paths): (Vec<_>, Vec<_>) = paths .iter() .map(|path| { let path = path.compact(); @@ -323,22 +415,58 @@ impl PickerDelegate for RecentProjectsDelegate { .unzip(); let highlighted_match = HighlightedMatchWithPaths { - match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", "), + match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", ").color( + if matches!(dev_server_status, Some(DevServerStatus::Offline)) { + Color::Disabled + } else { + Color::Default + }, + ), paths, }; Some( ListItem::new(ix) + .selected(selected) .inset(true) .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child({ - let mut highlighted = highlighted_match.clone(); - if !self.render_paths { - highlighted.paths.clear(); - } - highlighted.render(cx) - }) + .child( + h_flex() + .flex_grow() + .gap_3() + .when(self.has_any_remote_projects, |this| { + this.child(if is_remote { + // if disabled, Color::Disabled + let indicator_color = match dev_server_status { + Some(DevServerStatus::Online) => Color::Created, + Some(DevServerStatus::Offline) => Color::Hidden, + _ => unreachable!(), + }; + IconWithIndicator::new( + Icon::new(IconName::Server).color(Color::Muted), + Some(Indicator::dot()), + ) + .indicator_color(indicator_color) + .indicator_border_color(if selected { + Some(cx.theme().colors().element_selected) + } else { + None + }) + .into_any_element() + } else { + Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element() + }) + }) + .child({ + let mut highlighted = highlighted_match.clone(); + if !self.render_paths { + highlighted.paths.clear(); + } + highlighted.render(cx) + }), + ) .when(!is_current_workspace, |el| { let delete_button = div() .child( @@ -369,6 +497,39 @@ impl PickerDelegate for RecentProjectsDelegate { }), ) } + + fn render_footer(&self, cx: &mut ViewContext>) -> Option { + if !cx.has_flag::() { + return None; + } + Some( + h_flex() + .border_t_1() + .py_2() + .pr_2() + .border_color(cx.theme().colors().border) + .justify_end() + .gap_4() + .child( + ButtonLike::new("remote") + .when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| { + button.child(key) + }) + .child(Label::new("Connect remote…").color(Color::Muted)) + .on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())), + ) + .child( + ButtonLike::new("local") + .when_some( + KeyBinding::for_action(&workspace::Open, cx), + |button, key| button.child(key), + ) + .child(Label::new("Open folder…").color(Color::Muted)) + .on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())), + ) + .into_any(), + ) + } } // Compute the highlighted text for the name and path @@ -406,6 +567,7 @@ fn highlights_for_path( text: text.to_string(), highlight_positions, char_count, + color: Color::Default, } }); @@ -415,6 +577,7 @@ fn highlights_for_path( text: path_string.to_string(), highlight_positions: path_positions, char_count: path_char_count, + color: Color::Default, }, ) } @@ -430,7 +593,7 @@ impl RecentProjectsDelegate { .await .unwrap_or_default(); this.update(&mut cx, move |picker, cx| { - picker.delegate.workspaces = workspaces; + picker.delegate.set_workspaces(workspaces); picker.delegate.set_selected_index(ix - 1, cx); picker.delegate.reset_selected_match_index = false; picker.update_matches(picker.query(cx), cx) @@ -475,7 +638,7 @@ mod tests { use gpui::{TestAppContext, WindowHandle}; use project::Project; use serde_json::json; - use workspace::{open_paths, AppState}; + use workspace::{open_paths, AppState, LocalPaths}; use super::*; @@ -539,10 +702,10 @@ mod tests { positions: Vec::new(), string: "fake candidate".to_string(), }]; - delegate.workspaces = vec![( + delegate.set_workspaces(vec![( WorkspaceId::default(), - WorkspaceLocation::new(vec!["/test/path/"]), - )]; + LocalPaths::new(vec!["/test/path/"]).into(), + )]); }); }) .unwrap(); diff --git a/crates/recent_projects/src/remote_projects.rs b/crates/recent_projects/src/remote_projects.rs new file mode 100644 index 0000000000..2a2f13d945 --- /dev/null +++ b/crates/recent_projects/src/remote_projects.rs @@ -0,0 +1,749 @@ +use std::time::Duration; + +use feature_flags::FeatureFlagViewExt; +use gpui::{ + percentage, Action, Animation, AnimationExt, AppContext, ClipboardItem, DismissEvent, + EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, + ViewContext, +}; +use remote_projects::{DevServer, DevServerId, RemoteProject, RemoteProjectId}; +use rpc::{ + proto::{self, CreateDevServerResponse, DevServerStatus}, + ErrorCode, ErrorExt, +}; +use settings::Settings; +use theme::ThemeSettings; +use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip}; +use ui_text_field::{FieldLabelLayout, TextField}; +use util::ResultExt; +use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace}; + +use crate::OpenRemote; + +pub struct RemoteProjects { + mode: Mode, + focus_handle: FocusHandle, + scroll_handle: ScrollHandle, + remote_project_store: Model, + remote_project_path_input: View, + dev_server_name_input: View, + _subscription: gpui::Subscription, +} + +#[derive(Default)] +struct CreateDevServer { + creating: bool, + dev_server: Option, +} + +struct CreateRemoteProject { + dev_server_id: DevServerId, + creating: bool, + remote_project: Option, +} + +enum Mode { + Default, + CreateRemoteProject(CreateRemoteProject), + CreateDevServer(CreateDevServer), +} + +impl RemoteProjects { + pub fn register(_: &mut Workspace, cx: &mut ViewContext) { + cx.observe_flag::(|enabled, workspace, _| { + if enabled { + workspace.register_action(|workspace, _: &OpenRemote, cx| { + workspace.toggle_modal(cx, |cx| Self::new(cx)) + }); + } + }) + .detach(); + } + + pub fn new(cx: &mut ViewContext) -> Self { + let remote_project_path_input = cx.new_view(|cx| TextField::new(cx, "", "Project path")); + let dev_server_name_input = + cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked)); + + let focus_handle = cx.focus_handle(); + let remote_project_store = remote_projects::Store::global(cx); + + let subscription = cx.observe(&remote_project_store, |_, _, cx| { + cx.notify(); + }); + + Self { + mode: Mode::Default, + focus_handle, + scroll_handle: ScrollHandle::new(), + remote_project_store, + remote_project_path_input, + dev_server_name_input, + _subscription: subscription, + } + } + + pub fn create_remote_project( + &mut self, + dev_server_id: DevServerId, + cx: &mut ViewContext, + ) { + let path = self + .remote_project_path_input + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + if path == "" { + return; + } + + if self + .remote_project_store + .read(cx) + .remote_projects_for_server(dev_server_id) + .iter() + .any(|p| p.path == path) + { + cx.spawn(|_, mut cx| async move { + cx.prompt( + gpui::PromptLevel::Critical, + "Failed to create project", + Some(&format!( + "Project {} already exists for this dev server.", + path + )), + &["Ok"], + ) + .await + }) + .detach_and_log_err(cx); + return; + } + + let create = { + let path = path.clone(); + self.remote_project_store.update(cx, |store, cx| { + store.create_remote_project(dev_server_id, path, cx) + }) + }; + + cx.spawn(|this, mut cx| async move { + let result = create.await; + let remote_project = result.as_ref().ok().and_then(|r| r.remote_project.clone()); + this.update(&mut cx, |this, _| { + this.mode = Mode::CreateRemoteProject(CreateRemoteProject { + dev_server_id, + creating: false, + remote_project, + }); + }) + .log_err(); + result + }) + .detach_and_prompt_err("Failed to create project", cx, move |e, _| { + match e.error_code() { + ErrorCode::DevServerOffline => Some( + "The dev server is offline. Please log in and check it is connected." + .to_string(), + ), + ErrorCode::RemoteProjectPathDoesNotExist => { + Some(format!("The path `{}` does not exist on the server.", path)) + } + _ => None, + } + }); + + self.remote_project_path_input.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("", cx); + }); + }); + + self.mode = Mode::CreateRemoteProject(CreateRemoteProject { + dev_server_id, + creating: true, + remote_project: None, + }); + } + + pub fn create_dev_server(&mut self, cx: &mut ViewContext) { + let name = self + .dev_server_name_input + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + if name == "" { + return; + } + + let dev_server = self + .remote_project_store + .update(cx, |store, cx| store.create_dev_server(name.clone(), cx)); + + cx.spawn(|this, mut cx| async move { + let result = dev_server.await; + + this.update(&mut cx, |this, _| match &result { + Ok(dev_server) => { + this.mode = Mode::CreateDevServer(CreateDevServer { + creating: false, + dev_server: Some(dev_server.clone()), + }); + } + Err(_) => { + this.mode = Mode::CreateDevServer(Default::default()); + } + }) + .log_err(); + result + }) + .detach_and_prompt_err("Failed to create server", cx, |_, _| None); + + self.mode = Mode::CreateDevServer(CreateDevServer { + creating: true, + dev_server: None, + }); + cx.notify() + } + + fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext) { + let answer = cx.prompt( + gpui::PromptLevel::Info, + "Are you sure?", + Some("This will delete the dev server and all of its remote projects."), + &["Delete", "Cancel"], + ); + + cx.spawn(|this, mut cx| async move { + let answer = answer.await?; + + if answer != 0 { + return Ok(()); + } + + this.update(&mut cx, |this, cx| { + this.remote_project_store + .update(cx, |store, cx| store.delete_dev_server(id, cx)) + })? + .await + }) + .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + match self.mode { + Mode::Default => {} + Mode::CreateRemoteProject(CreateRemoteProject { dev_server_id, .. }) => { + self.create_remote_project(dev_server_id, cx); + } + Mode::CreateDevServer(_) => { + self.create_dev_server(cx); + } + } + } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + match self.mode { + Mode::Default => cx.emit(DismissEvent), + Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => { + self.mode = Mode::Default; + self.focus_handle(cx).focus(cx); + cx.notify(); + } + } + } + + fn render_dev_server( + &mut self, + dev_server: &DevServer, + cx: &mut ViewContext, + ) -> impl IntoElement { + let dev_server_id = dev_server.id; + let status = dev_server.status; + + v_flex() + .w_full() + .child( + h_flex() + .group("dev-server") + .justify_between() + .child( + h_flex() + .gap_2() + .child( + div() + .id(("status", dev_server.id.0)) + .relative() + .child(Icon::new(IconName::Server).size(IconSize::Small)) + .child( + div().absolute().bottom_0().left(rems_from_px(8.0)).child( + Indicator::dot().color(match status { + DevServerStatus::Online => Color::Created, + DevServerStatus::Offline => Color::Hidden, + }), + ), + ) + .tooltip(move |cx| { + Tooltip::text( + match status { + DevServerStatus::Online => "Online", + DevServerStatus::Offline => "Offline", + }, + cx, + ) + }), + ) + .child(dev_server.name.clone()) + .child( + h_flex() + .visible_on_hover("dev-server") + .gap_1() + .child( + IconButton::new("edit-dev-server", IconName::Pencil) + .disabled(true) //TODO implement this on the collab side + .tooltip(|cx| { + Tooltip::text("Coming Soon - Edit dev server", cx) + }), + ) + .child({ + let dev_server_id = dev_server.id; + IconButton::new("remove-dev-server", IconName::Trash) + .on_click(cx.listener(move |this, _, cx| { + this.delete_dev_server(dev_server_id, cx) + })) + .tooltip(|cx| Tooltip::text("Remove dev server", cx)) + }), + ), + ) + .child( + h_flex().gap_1().child( + IconButton::new( + ("add-remote-project", dev_server_id.0), + IconName::Plus, + ) + .tooltip(|cx| Tooltip::text("Add a remote project", cx)) + .on_click(cx.listener( + move |this, _, cx| { + this.mode = Mode::CreateRemoteProject(CreateRemoteProject { + dev_server_id, + creating: false, + remote_project: None, + }); + this.remote_project_path_input + .read(cx) + .focus_handle(cx) + .focus(cx); + cx.notify(); + }, + )), + ), + ), + ) + .child( + v_flex() + .w_full() + .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct + .border() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .my_1() + .py_0p5() + .px_3() + .child( + List::new().empty_message("No projects.").children( + self.remote_project_store + .read(cx) + .remote_projects_for_server(dev_server.id) + .iter() + .map(|p| self.render_remote_project(p, cx)), + ), + ), + ) + } + + fn render_remote_project( + &mut self, + project: &RemoteProject, + cx: &mut ViewContext, + ) -> impl IntoElement { + let remote_project_id = project.id; + let project_id = project.project_id; + let is_online = project_id.is_some(); + + ListItem::new(("remote-project", remote_project_id.0)) + .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted))) + .child( + Label::new(project.path.clone()) + ) + .on_click(cx.listener(move |_, _, cx| { + if let Some(project_id) = project_id { + if let Some(app_state) = AppState::global(cx).upgrade() { + workspace::join_remote_project(project_id, app_state, cx) + .detach_and_prompt_err("Could not join project", cx, |_, _| None) + } + } else { + cx.spawn(|_, mut cx| async move { + cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err(); + }).detach(); + } + })) + } + + fn render_create_dev_server(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Mode::CreateDevServer(CreateDevServer { + creating, + dev_server, + }) = &self.mode + else { + unreachable!() + }; + + self.dev_server_name_input.update(cx, |input, cx| { + input.set_disabled(*creating || dev_server.is_some(), cx); + }); + + v_flex() + .id("scroll-container") + .h_full() + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .px_1() + .pt_0p5() + .gap_px() + .child( + ModalHeader::new("remote-projects") + .show_back_button(true) + .child(Headline::new("New dev server").size(HeadlineSize::Small)), + ) + .child( + ModalContent::new().child( + v_flex() + .w_full() + .child( + h_flex() + .pb_2() + .items_end() + .w_full() + .px_2() + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + div() + .pl_2() + .max_w(rems(16.)) + .child(self.dev_server_name_input.clone()), + ) + .child( + div() + .pl_1() + .pb(px(3.)) + .when(!*creating && dev_server.is_none(), |div| { + div.child(Button::new("create-dev-server", "Create").on_click( + cx.listener(move |this, _, cx| { + this.create_dev_server(cx); + }), + )) + }) + .when(*creating && dev_server.is_none(), |div| { + div.child( + Button::new("create-dev-server", "Creating...") + .disabled(true), + ) + }), + ) + ) + .when(dev_server.is_none(), |div| { + div.px_2().child(Label::new("Once you have created a dev server, you will be given a command to run on the server to register it.").color(Color::Muted)) + }) + .when_some(dev_server.clone(), |div, dev_server| { + let status = self + .remote_project_store + .read(cx) + .dev_server_status(DevServerId(dev_server.dev_server_id)); + + let instructions = SharedString::from(format!( + "zed --dev-server-token {}", + dev_server.access_token + )); + div.child( + v_flex() + .pl_2() + .pt_2() + .gap_2() + .child( + h_flex().justify_between().w_full() + .child(Label::new(format!( + "Please log into `{}` and run:", + dev_server.name + ))) + .child( + Button::new("copy-access-token", "Copy Instructions") + .icon(Some(IconName::Copy)) + .icon_size(IconSize::Small) + .on_click({ + let instructions = instructions.clone(); + cx.listener(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new( + instructions.to_string(), + )) + })}) + ) + ) + .child( + v_flex() + .w_full() + .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct + .border() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .my_1() + .py_0p5() + .px_3() + .font(ThemeSettings::get_global(cx).buffer_font.family.clone()) + .child(Label::new(instructions)) + ) + .when(status == DevServerStatus::Offline, |this| { + this.child( + + h_flex() + .gap_2() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Medium) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ), + ) + .child( + Label::new("Waiting for connection…"), + ) + ) + }) + .when(status == DevServerStatus::Online, |this| { + this.child(Label::new("🎊 Connection established!")) + .child( + h_flex().justify_end().child( + Button::new("done", "Done").on_click(cx.listener( + |_, _, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()) + }, + )) + ), + ) + }), + ) + }), + ) + ) + } + + fn render_default(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let dev_servers = self.remote_project_store.read(cx).dev_servers(); + + v_flex() + .id("scroll-container") + .h_full() + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .px_1() + .pt_0p5() + .gap_px() + .child( + ModalHeader::new("remote-projects") + .show_dismiss_button(true) + .child(Headline::new("Remote Projects").size(HeadlineSize::Small)), + ) + .child( + ModalContent::new().child( + List::new() + .empty_message("No dev servers registered.") + .header(Some( + ListHeader::new("Dev Servers").end_slot( + Button::new("register-dev-server-button", "New Server") + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .tooltip(|cx| Tooltip::text("Register a new dev server", cx)) + .on_click(cx.listener(|this, _, cx| { + this.mode = Mode::CreateDevServer(Default::default()); + + this.dev_server_name_input.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("", cx); + }); + input.focus_handle(cx).focus(cx) + }); + + cx.notify(); + })), + ), + )) + .children(dev_servers.iter().map(|dev_server| { + self.render_dev_server(dev_server, cx).into_any_element() + })), + ), + ) + } + + fn render_create_remote_project(&self, cx: &mut ViewContext) -> impl IntoElement { + let Mode::CreateRemoteProject(CreateRemoteProject { + dev_server_id, + creating, + remote_project, + }) = &self.mode + else { + unreachable!() + }; + + let dev_server = self + .remote_project_store + .read(cx) + .dev_server(*dev_server_id) + .cloned(); + + let (dev_server_name, dev_server_status) = dev_server + .map(|server| (server.name, server.status)) + .unwrap_or((SharedString::from(""), DevServerStatus::Offline)); + + v_flex() + .px_1() + .pt_0p5() + .gap_px() + .child( + v_flex().py_0p5().px_1().child( + h_flex() + .px_1() + .py_0p5() + .child( + IconButton::new("back", IconName::ArrowLeft) + .style(ButtonStyle::Transparent) + .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()) + })), + ) + .child(Headline::new("Add remote project").size(HeadlineSize::Small)), + ), + ) + .child( + h_flex() + .ml_5() + .gap_2() + .child( + div() + .id(("status", dev_server_id.0)) + .relative() + .child(Icon::new(IconName::Server)) + .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child( + Indicator::dot().color(match dev_server_status { + DevServerStatus::Online => Color::Created, + DevServerStatus::Offline => Color::Hidden, + }), + )) + .tooltip(move |cx| { + Tooltip::text( + match dev_server_status { + DevServerStatus::Online => "Online", + DevServerStatus::Offline => "Offline", + }, + cx, + ) + }), + ) + .child(dev_server_name.clone()), + ) + .child( + h_flex() + .ml_5() + .gap_2() + .child(self.remote_project_path_input.clone()) + .when(!*creating && remote_project.is_none(), |div| { + div.child(Button::new("create-remote-server", "Create").on_click({ + let dev_server_id = *dev_server_id; + cx.listener(move |this, _, cx| { + this.create_remote_project(dev_server_id, cx) + }) + })) + }) + .when(*creating, |div| { + div.child(Button::new("create-dev-server", "Creating...").disabled(true)) + }), + ) + .when_some(remote_project.clone(), |div, remote_project| { + let status = self + .remote_project_store + .read(cx) + .remote_project(RemoteProjectId(remote_project.id)) + .map(|project| { + if project.project_id.is_some() { + DevServerStatus::Online + } else { + DevServerStatus::Offline + } + }) + .unwrap_or(DevServerStatus::Offline); + div.child( + v_flex() + .ml_5() + .ml_8() + .gap_2() + .when(status == DevServerStatus::Offline, |this| { + this.child(Label::new("Waiting for project...")) + }) + .when(status == DevServerStatus::Online, |this| { + this.child(Label::new("Project online! 🎊")).child( + Button::new("done", "Done").on_click(cx.listener(|_, _, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()) + })), + ) + }), + ) + }) + } +} +impl ModalView for RemoteProjects {} + +impl FocusableView for RemoteProjects { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for RemoteProjects {} + +impl Render for RemoteProjects { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .track_focus(&self.focus_handle) + .elevation_3(cx) + .key_context("DevServerModal") + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::confirm)) + .on_mouse_down_out(cx.listener(|this, _, cx| { + if matches!(this.mode, Mode::Default) { + cx.emit(DismissEvent) + } + })) + .pb_4() + .w(rems(34.)) + .min_h(rems(20.)) + .max_h(rems(40.)) + .child(match &self.mode { + Mode::Default => self.render_default(cx).into_any_element(), + Mode::CreateRemoteProject(_) => { + self.render_create_remote_project(cx).into_any_element() + } + Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(), + }) + } +} diff --git a/crates/remote_projects/Cargo.toml b/crates/remote_projects/Cargo.toml new file mode 100644 index 0000000000..2e904f3326 --- /dev/null +++ b/crates/remote_projects/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "remote_projects" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/remote_projects.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +serde.workspace = true +client.workspace = true +rpc.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/remote_projects/src/remote_projects.rs b/crates/remote_projects/src/remote_projects.rs new file mode 100644 index 0000000000..5e62d5c32e --- /dev/null +++ b/crates/remote_projects/src/remote_projects.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, SharedString, Task}; +use rpc::{ + proto::{self, DevServerStatus}, + TypedEnvelope, +}; +use std::{collections::HashMap, sync::Arc}; + +use client::{Client, ProjectId}; +pub use client::{DevServerId, RemoteProjectId}; + +pub struct Store { + remote_projects: HashMap, + dev_servers: HashMap, + _subscriptions: Vec, + client: Arc, +} + +#[derive(Debug, Clone)] +pub struct RemoteProject { + pub id: RemoteProjectId, + pub project_id: Option, + pub path: SharedString, + pub dev_server_id: DevServerId, +} + +impl From for RemoteProject { + fn from(project: proto::RemoteProject) -> Self { + Self { + id: RemoteProjectId(project.id), + project_id: project.project_id.map(|id| ProjectId(id)), + path: project.path.into(), + dev_server_id: DevServerId(project.dev_server_id), + } + } +} + +#[derive(Debug, Clone)] +pub struct DevServer { + pub id: DevServerId, + pub name: SharedString, + pub status: DevServerStatus, +} + +impl From for DevServer { + fn from(dev_server: proto::DevServer) -> Self { + Self { + id: DevServerId(dev_server.dev_server_id), + status: dev_server.status(), + name: dev_server.name.into(), + } + } +} + +struct GlobalStore(Model); + +impl Global for GlobalStore {} + +pub fn init(client: Arc, cx: &mut AppContext) { + let store = cx.new_model(|cx| Store::new(client, cx)); + cx.set_global(GlobalStore(store)); +} + +impl Store { + pub fn global(cx: &AppContext) -> Model { + cx.global::().0.clone() + } + + pub fn new(client: Arc, cx: &ModelContext) -> Self { + Self { + remote_projects: Default::default(), + dev_servers: Default::default(), + _subscriptions: vec![ + client.add_message_handler(cx.weak_model(), Self::handle_remote_projects_update) + ], + client, + } + } + + pub fn remote_projects_for_server(&self, id: DevServerId) -> Vec { + let mut projects: Vec = self + .remote_projects + .values() + .filter(|project| project.dev_server_id == id) + .cloned() + .collect(); + projects.sort_by_key(|p| (p.path.clone(), p.id)); + projects + } + + pub fn dev_servers(&self) -> Vec { + let mut dev_servers: Vec = self.dev_servers.values().cloned().collect(); + dev_servers.sort_by_key(|d| (d.status == DevServerStatus::Offline, d.name.clone(), d.id)); + dev_servers + } + + pub fn dev_server(&self, id: DevServerId) -> Option<&DevServer> { + self.dev_servers.get(&id) + } + + pub fn dev_server_status(&self, id: DevServerId) -> DevServerStatus { + self.dev_server(id) + .map(|server| server.status) + .unwrap_or(DevServerStatus::Offline) + } + + pub fn remote_projects(&self) -> Vec { + let mut projects: Vec = self.remote_projects.values().cloned().collect(); + projects.sort_by_key(|p| (p.path.clone(), p.id)); + projects + } + + pub fn remote_project(&self, id: RemoteProjectId) -> Option<&RemoteProject> { + self.remote_projects.get(&id) + } + + async fn handle_remote_projects_update( + this: Model, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.dev_servers = envelope + .payload + .dev_servers + .into_iter() + .map(|dev_server| (DevServerId(dev_server.dev_server_id), dev_server.into())) + .collect(); + this.remote_projects = envelope + .payload + .remote_projects + .into_iter() + .map(|project| (RemoteProjectId(project.id), project.into())) + .collect(); + + cx.notify(); + })?; + Ok(()) + } + + pub fn create_remote_project( + &mut self, + dev_server_id: DevServerId, + path: String, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::CreateRemoteProject { + dev_server_id: dev_server_id.0, + path, + }) + .await + }) + } + + pub fn create_dev_server( + &mut self, + name: String, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + let result = client.request(proto::CreateDevServer { name }).await?; + Ok(result) + }) + } + + pub fn delete_dev_server( + &mut self, + id: DevServerId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::DeleteDevServer { + dev_server_id: id.0, + }) + .await?; + Ok(()) + }) + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c2c0363efa..7832af4b04 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -233,6 +233,10 @@ message Envelope { JoinRemoteProject join_remote_project = 185; RejoinRemoteProjects rejoin_remote_projects = 186; RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187; + + RemoteProjectsUpdate remote_projects_update = 193; + ValidateRemoteProjectRequest validate_remote_project_request = 194; // Current max + DeleteDevServer delete_dev_server = 195; } reserved 158 to 161; @@ -269,6 +273,8 @@ enum ErrorCode { UnsharedItem = 12; NoSuchProject = 13; DevServerAlreadyOnline = 14; + DevServerOffline = 15; + RemoteProjectPathDoesNotExist = 16; reserved 6; } @@ -433,6 +439,7 @@ message LiveKitConnectionInfo { message ShareProject { uint64 room_id = 1; repeated WorktreeMetadata worktrees = 2; + optional uint64 remote_project_id = 3; } message ShareProjectResponse { @@ -457,8 +464,8 @@ message JoinHostedProject { } message CreateRemoteProject { - uint64 channel_id = 1; - string name = 2; + reserved 1; + reserved 2; uint64 dev_server_id = 3; string path = 4; } @@ -466,14 +473,18 @@ message CreateRemoteProjectResponse { RemoteProject remote_project = 1; } +message ValidateRemoteProjectRequest { + string path = 1; +} + message CreateDevServer { - uint64 channel_id = 1; + reserved 1; string name = 2; } message CreateDevServerResponse { uint64 dev_server_id = 1; - uint64 channel_id = 2; + reserved 2; string access_token = 3; string name = 4; } @@ -481,6 +492,10 @@ message CreateDevServerResponse { message ShutdownDevServer { } +message DeleteDevServer { + uint64 dev_server_id = 1; +} + message ReconnectDevServer { repeated UpdateProject reshared_projects = 1; } @@ -493,6 +508,11 @@ message DevServerInstructions { repeated RemoteProject projects = 1; } +message RemoteProjectsUpdate { + repeated DevServer dev_servers = 1; + repeated RemoteProject remote_projects = 2; +} + message ShareRemoteProject { uint64 remote_project_id = 1; repeated WorktreeMetadata worktrees = 2; @@ -509,6 +529,7 @@ message JoinProjectResponse { repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; ChannelRole role = 6; + optional uint64 remote_project_id = 7; } message LeaveProject { @@ -1131,11 +1152,10 @@ message UpdateChannels { repeated HostedProject hosted_projects = 10; repeated uint64 deleted_hosted_projects = 11; - repeated DevServer dev_servers = 12; - repeated uint64 deleted_dev_servers = 13; - - repeated RemoteProject remote_projects = 14; - repeated uint64 deleted_remote_projects = 15; + reserved 12; + reserved 13; + reserved 14; + reserved 15; } message UpdateUserChannels { @@ -1174,14 +1194,14 @@ message HostedProject { message RemoteProject { uint64 id = 1; optional uint64 project_id = 2; - uint64 channel_id = 3; - string name = 4; + reserved 3; + reserved 4; uint64 dev_server_id = 5; string path = 6; } message DevServer { - uint64 channel_id = 1; + reserved 1; uint64 dev_server_id = 2; string name = 3; DevServerStatus status = 4; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 48160b2fe4..25074083f3 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -303,7 +303,7 @@ messages!( (SetRoomParticipantRole, Foreground), (BlameBuffer, Foreground), (BlameBufferResponse, Foreground), - (CreateRemoteProject, Foreground), + (CreateRemoteProject, Background), (CreateRemoteProjectResponse, Foreground), (CreateDevServer, Foreground), (CreateDevServerResponse, Foreground), @@ -317,6 +317,9 @@ messages!( (RejoinRemoteProjectsResponse, Foreground), (MultiLspQuery, Background), (MultiLspQueryResponse, Background), + (RemoteProjectsUpdate, Foreground), + (ValidateRemoteProjectRequest, Background), + (DeleteDevServer, Foreground) ); request_messages!( @@ -417,7 +420,9 @@ request_messages!( (JoinRemoteProject, JoinProjectResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), (ReconnectDevServer, ReconnectDevServerResponse), + (ValidateRemoteProjectRequest, Ack), (MultiLspQuery, MultiLspQueryResponse), + (DeleteDevServer, Ack), ); entity_messages!( diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index 55ba5f8294..f88d37c7e0 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -105,51 +105,50 @@ impl Connection { let mut raw_statement = ptr::null_mut::(); let mut remaining_sql_ptr = ptr::null(); - let (res, offset, message, _conn) = if let Some(table_to_alter) = alter_table { - // ALTER TABLE is a weird statement. When preparing the statement the table's - // existence is checked *before* syntax checking any other part of the statement. - // Therefore, we need to make sure that the table has been created before calling - // prepare. As we don't want to trash whatever database this is connected to, we - // create a new in-memory DB to test. + let (res, offset, message, _conn) = + if let Some((table_to_alter, column)) = alter_table { + // ALTER TABLE is a weird statement. When preparing the statement the table's + // existence is checked *before* syntax checking any other part of the statement. + // Therefore, we need to make sure that the table has been created before calling + // prepare. As we don't want to trash whatever database this is connected to, we + // create a new in-memory DB to test. - let temp_connection = Connection::open_memory(None); - //This should always succeed, if it doesn't then you really should know about it - temp_connection - .exec(&format!( - "CREATE TABLE {table_to_alter}(__place_holder_column_for_syntax_checking)" - )) - .unwrap()() - .unwrap(); + let temp_connection = Connection::open_memory(None); + //This should always succeed, if it doesn't then you really should know about it + temp_connection + .exec(&format!("CREATE TABLE {table_to_alter}({column})")) + .unwrap()() + .unwrap(); - sqlite3_prepare_v2( - temp_connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ); + sqlite3_prepare_v2( + temp_connection.sqlite3, + remaining_sql.as_ptr(), + -1, + &mut raw_statement, + &mut remaining_sql_ptr, + ); - ( - sqlite3_errcode(temp_connection.sqlite3), - sqlite3_error_offset(temp_connection.sqlite3), - sqlite3_errmsg(temp_connection.sqlite3), - Some(temp_connection), - ) - } else { - sqlite3_prepare_v2( - self.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ); - ( - sqlite3_errcode(self.sqlite3), - sqlite3_error_offset(self.sqlite3), - sqlite3_errmsg(self.sqlite3), - None, - ) - }; + ( + sqlite3_errcode(temp_connection.sqlite3), + sqlite3_error_offset(temp_connection.sqlite3), + sqlite3_errmsg(temp_connection.sqlite3), + Some(temp_connection), + ) + } else { + sqlite3_prepare_v2( + self.sqlite3, + remaining_sql.as_ptr(), + -1, + &mut raw_statement, + &mut remaining_sql_ptr, + ); + ( + sqlite3_errcode(self.sqlite3), + sqlite3_error_offset(self.sqlite3), + sqlite3_errmsg(self.sqlite3), + None, + ) + }; sqlite3_finalize(raw_statement); @@ -203,7 +202,7 @@ impl Connection { } } -fn parse_alter_table(remaining_sql_str: &str) -> Option { +fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> { let remaining_sql_str = remaining_sql_str.to_lowercase(); if remaining_sql_str.starts_with("alter") { if let Some(table_offset) = remaining_sql_str.find("table") { @@ -215,7 +214,19 @@ fn parse_alter_table(remaining_sql_str: &str) -> Option { .take_while(|c| !c.is_whitespace()) .collect::(); if !table_to_alter.is_empty() { - return Some(table_to_alter); + let column_name = + if let Some(rename_offset) = remaining_sql_str.find("rename column") { + let after_rename_offset = rename_offset + "rename column".len(); + remaining_sql_str + .chars() + .skip(after_rename_offset) + .skip_while(|c| c.is_whitespace()) + .take_while(|c| !c.is_whitespace()) + .collect::() + } else { + "__place_holder_column_for_syntax_checking".to_string() + }; + return Some((table_to_alter, column_name)); } } } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index 122b6d0c58..462f902239 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -320,6 +320,7 @@ impl<'a> Statement<'a> { this: &mut Statement, callback: impl FnOnce(&mut Statement) -> Result, ) -> Result { + println!("{:?}", std::any::type_name::()); if this.step()? != StepResult::Row { return Err(anyhow!("single called with query that returns no rows.")); } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index ab04cd5909..d63b56b015 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -330,6 +330,7 @@ impl PickerDelegate for TasksModalDelegate { text: hit.string.clone(), highlight_positions: hit.positions.clone(), char_count: hit.string.chars().count(), + color: Color::Default, }; let icon = match source_kind { TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)), diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index f31db36204..ef53ab109e 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,7 +1,7 @@ -use gpui::{svg, IntoElement, Rems, Transformation}; +use gpui::{svg, Hsla, IntoElement, Rems, Transformation}; use strum::EnumIter; -use crate::prelude::*; +use crate::{prelude::*, Indicator}; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { @@ -283,3 +283,63 @@ impl RenderOnce for Icon { .text_color(self.color.color(cx)) } } + +#[derive(IntoElement)] +pub struct IconWithIndicator { + icon: Icon, + indicator: Option, + indicator_border_color: Option, +} + +impl IconWithIndicator { + pub fn new(icon: Icon, indicator: Option) -> Self { + Self { + icon, + indicator, + indicator_border_color: None, + } + } + + pub fn indicator(mut self, indicator: Option) -> Self { + self.indicator = indicator; + self + } + + pub fn indicator_color(mut self, color: Color) -> Self { + if let Some(indicator) = self.indicator.as_mut() { + indicator.color = color; + } + self + } + + pub fn indicator_border_color(mut self, color: Option) -> Self { + self.indicator_border_color = color; + self + } +} + +impl RenderOnce for IconWithIndicator { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let indicator_border_color = self + .indicator_border_color + .unwrap_or_else(|| cx.theme().colors().elevated_surface_background); + + div() + .relative() + .child(self.icon) + .when_some(self.indicator, |this, indicator| { + this.child( + div() + .absolute() + .w_2() + .h_2() + .border() + .border_color(indicator_border_color) + .rounded_full() + .neg_bottom_0p5() + .neg_right_1() + .child(indicator), + ) + }) + } +} diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 7ce9707b0a..107f61d5b4 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -1,12 +1,16 @@ -use gpui::*; +use gpui::{prelude::FluentBuilder, *}; use smallvec::SmallVec; -use crate::{h_flex, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize}; +use crate::{ + h_flex, Clickable, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize, +}; #[derive(IntoElement)] pub struct ModalHeader { id: ElementId, children: SmallVec<[AnyElement; 2]>, + show_dismiss_button: bool, + show_back_button: bool, } impl ModalHeader { @@ -14,8 +18,20 @@ impl ModalHeader { Self { id: id.into(), children: SmallVec::new(), + show_dismiss_button: false, + show_back_button: false, } } + + pub fn show_dismiss_button(mut self, show: bool) -> Self { + self.show_dismiss_button = show; + self + } + + pub fn show_back_button(mut self, show: bool) -> Self { + self.show_back_button = show; + self + } } impl ParentElement for ModalHeader { @@ -31,9 +47,28 @@ impl RenderOnce for ModalHeader { .w_full() .px_2() .py_1p5() + .when(self.show_back_button, |this| { + this.child( + div().pr_1().child( + IconButton::new("back", IconName::ArrowLeft) + .shape(IconButtonShape::Square) + .on_click(|_, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()); + }), + ), + ) + }) .child(div().flex_1().children(self.children)) .justify_between() - .child(IconButton::new("dismiss", IconName::Close).shape(IconButtonShape::Square)) + .when(self.show_dismiss_button, |this| { + this.child( + IconButton::new("dismiss", IconName::Close) + .shape(IconButtonShape::Square) + .on_click(|_, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()); + }), + ) + }) } } diff --git a/crates/ui_text_field/src/ui_text_field.rs b/crates/ui_text_field/src/ui_text_field.rs index 3e5a347a6d..f5addf3a9f 100644 --- a/crates/ui_text_field/src/ui_text_field.rs +++ b/crates/ui_text_field/src/ui_text_field.rs @@ -44,6 +44,8 @@ pub struct TextField { start_icon: Option, /// The layout of the label relative to the text field. with_label: FieldLabelLayout, + /// Whether the text field is disabled. + disabled: bool, } impl FocusableView for TextField { @@ -72,6 +74,7 @@ impl TextField { editor, start_icon: None, with_label: FieldLabelLayout::Hidden, + disabled: false, } } @@ -84,6 +87,16 @@ impl TextField { self.with_label = layout; self } + + pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext) { + self.disabled = disabled; + self.editor + .update(cx, |editor, _| editor.set_read_only(disabled)) + } + + pub fn editor(&self) -> &View { + &self.editor + } } impl Render for TextField { @@ -91,17 +104,17 @@ impl Render for TextField { let settings = ThemeSettings::get_global(cx); let theme_color = cx.theme().colors(); - let style = TextFieldStyle { + let mut style = TextFieldStyle { text_color: theme_color.text, background_color: theme_color.ghost_element_background, border_color: theme_color.border, }; - // if self.disabled { - // style.text_color = theme_color.text_disabled; - // style.background_color = theme_color.ghost_element_disabled; - // style.border_color = theme_color.border_disabled; - // } + if self.disabled { + style.text_color = theme_color.text_disabled; + style.background_color = theme_color.ghost_element_disabled; + style.border_color = theme_color.border_disabled; + } // if self.error_message.is_some() { // style.text_color = cx.theme().status().error; @@ -131,7 +144,15 @@ impl Render for TextField { .group("text-field") .w_full() .when(self.with_label == FieldLabelLayout::Stacked, |this| { - this.child(Label::new(self.label.clone()).size(LabelSize::Default)) + this.child( + Label::new(self.label.clone()) + .size(LabelSize::Default) + .color(if self.disabled { + Color::Disabled + } else { + Color::Muted + }), + ) }) .child( v_flex().w_full().child( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 608efb23c7..3b5a6be2b5 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -45,6 +45,7 @@ node_runtime.workspace = true parking_lot.workspace = true postage.workspace = true project.workspace = true +remote_projects.workspace = true task.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 130f9b038e..15184a9d3b 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -513,8 +513,9 @@ impl ItemHandle for View { })); } - let mut event_subscription = - Some(cx.subscribe(self, move |workspace, item, event, cx| { + let mut event_subscription = Some(cx.subscribe( + self, + move |workspace, item: View, event, cx| { let pane = if let Some(pane) = workspace .panes_by_item .get(&item.item_id()) @@ -575,7 +576,8 @@ impl ItemHandle for View { _ => {} }); - })); + }, + )); cx.on_blur(&self.focus_handle(cx), move |workspace, cx| { if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 3c4efa7669..2e507dc1bb 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -3,6 +3,7 @@ pub mod model; use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; +use client::RemoteProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds}; @@ -17,11 +18,11 @@ use uuid::Uuid; use crate::WorkspaceId; use model::{ - GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - WorkspaceLocation, + GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedWorkspace, }; -use self::model::DockStructure; +use self::model::{DockStructure, SerializedRemoteProject, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -125,7 +126,7 @@ define_connection! { // // workspaces( // workspace_id: usize, // Primary key for workspaces - // workspace_location: Bincode>, + // local_paths: Bincode>, // dock_visible: bool, // Deprecated // dock_anchor: DockAnchor, // Deprecated // dock_pane: Option, // Deprecated @@ -289,6 +290,15 @@ define_connection! { sql!( ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), ]; } @@ -300,13 +310,23 @@ impl WorkspaceDb { &self, worktree_roots: &[P], ) -> Option { - let workspace_location: WorkspaceLocation = worktree_roots.into(); + let local_paths = LocalPaths::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace - let (workspace_id, workspace_location, bounds, display, fullscreen, centered_layout, docks): ( + let ( + workspace_id, + local_paths, + remote_project_id, + bounds, + display, + fullscreen, + centered_layout, + docks, + ): ( WorkspaceId, - WorkspaceLocation, + Option, + Option, Option, Option, Option, @@ -316,7 +336,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - workspace_location, + local_paths, + remote_project_id, window_state, window_x, window_y, @@ -335,16 +356,34 @@ impl WorkspaceDb { bottom_dock_active_panel, bottom_dock_zoom FROM workspaces - WHERE workspace_location = ? + WHERE local_paths = ? }) - .and_then(|mut prepared_statement| (prepared_statement)(&workspace_location)) + .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; + let location = if let Some(remote_project_id) = remote_project_id { + let remote_project: SerializedRemoteProject = self + .select_row_bound(sql! { + SELECT remote_project_id, path, dev_server_name + FROM remote_projects + WHERE remote_project_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(remote_project_id)) + .context("No remote project found") + .warn_on_err() + .flatten()?; + SerializedWorkspaceLocation::Remote(remote_project) + } else if let Some(local_paths) = local_paths { + SerializedWorkspaceLocation::Local(local_paths) + } else { + return None; + }; + Some(SerializedWorkspace { id: workspace_id, - location: workspace_location.clone(), + location, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -368,43 +407,102 @@ impl WorkspaceDb { DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound(sql!( - DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? - ))?((&workspace.location, workspace.id)) - .context("clearing out old locations")?; + match workspace.location { + SerializedWorkspaceLocation::Local(local_paths) => { + conn.exec_bound(sql!( + DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? + ))?((&local_paths, workspace.id)) + .context("clearing out old locations")?; - // Upsert - conn.exec_bound(sql!( - INSERT INTO workspaces( - workspace_id, - workspace_location, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - workspace_location = ?2, - left_dock_visible = ?3, - left_dock_active_panel = ?4, - left_dock_zoom = ?5, - right_dock_visible = ?6, - right_dock_active_panel = ?7, - right_dock_zoom = ?8, - bottom_dock_visible = ?9, - bottom_dock_active_panel = ?10, - bottom_dock_zoom = ?11, - timestamp = CURRENT_TIMESTAMP - ))?((workspace.id, &workspace.location, workspace.docks)) - .context("Updating workspace")?; + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + local_paths, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + local_paths = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + timestamp = CURRENT_TIMESTAMP + ))?((workspace.id, &local_paths, workspace.docks)) + .context("Updating workspace")?; + } + SerializedWorkspaceLocation::Remote(remote_project) => { + conn.exec_bound(sql!( + DELETE FROM workspaces WHERE remote_project_id = ? AND workspace_id != ? + ))?((remote_project.id.0, workspace.id)) + .context("clearing out old locations")?; + + conn.exec_bound(sql!( + INSERT INTO remote_projects( + remote_project_id, + path, + dev_server_name + ) VALUES (?1, ?2, ?3) + ON CONFLICT DO + UPDATE SET + path = ?2, + dev_server_name = ?3 + ))?(&remote_project)?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + remote_project_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + remote_project_id = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + timestamp = CURRENT_TIMESTAMP + ))?(( + workspace.id, + remote_project.id.0, + workspace.docks, + )) + .context("Updating workspace")?; + } + } // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -424,24 +522,43 @@ impl WorkspaceDb { } query! { - fn recent_workspaces() -> Result> { - SELECT workspace_id, workspace_location + fn recent_workspaces() -> Result)>> { + SELECT workspace_id, local_paths, remote_project_id FROM workspaces - WHERE workspace_location IS NOT NULL + WHERE local_paths IS NOT NULL OR remote_project_id IS NOT NULL ORDER BY timestamp DESC } } query! { - pub fn last_window() -> Result<(Option, Option, Option)> { - SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen - FROM workspaces - WHERE workspace_location IS NOT NULL - ORDER BY timestamp DESC - LIMIT 1 + fn remote_projects() -> Result> { + SELECT remote_project_id, path, dev_server_name + FROM remote_projects } } + pub(crate) fn last_window( + &self, + ) -> anyhow::Result<(Option, Option, Option)> { + let mut prepared_query = + self.select::<(Option, Option, Option)>(sql!( + SELECT + display, + window_state, window_x, window_y, window_width, window_height, + fullscreen + FROM workspaces + WHERE local_paths + IS NOT NULL + ORDER BY timestamp DESC + LIMIT 1 + ))?; + let result = prepared_query()?; + Ok(result + .into_iter() + .next() + .unwrap_or_else(|| (None, None, None))) + } + query! { pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { DELETE FROM workspaces @@ -451,14 +568,29 @@ impl WorkspaceDb { // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. - pub async fn recent_workspaces_on_disk(&self) -> Result> { + pub async fn recent_workspaces_on_disk( + &self, + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - for (id, location) in self.recent_workspaces()? { + let remote_projects = self.remote_projects()?; + + for (id, location, remote_project_id) in self.recent_workspaces()? { + if let Some(remote_project_id) = remote_project_id.map(RemoteProjectId) { + if let Some(remote_project) = + remote_projects.iter().find(|rp| rp.id == remote_project_id) + { + result.push((id, remote_project.clone().into())); + } else { + delete_tasks.push(self.delete_workspace_by_id(id)); + } + continue; + } + if location.paths().iter().all(|path| path.exists()) && location.paths().iter().any(|path| path.is_dir()) { - result.push((id, location)); + result.push((id, location.into())); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -468,13 +600,16 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() - .next() - .map(|(_, location)| location)) + .filter_map(|(_, location)| match location { + SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths), + SerializedWorkspaceLocation::Remote(_) => None, + }) + .next()) } fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { @@ -774,7 +909,7 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: (["/tmp", "/tmp2"]).into(), + location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group: Default::default(), bounds: Default::default(), display: Default::default(), @@ -785,7 +920,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: (["/tmp"]).into(), + location: LocalPaths::new(["/tmp"]).into(), center_group: Default::default(), bounds: Default::default(), display: Default::default(), @@ -812,7 +947,7 @@ mod tests { }) .await; - workspace_1.location = (["/tmp", "/tmp3"]).into(); + workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into(); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -885,7 +1020,7 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - location: (["/tmp", "/tmp2"]).into(), + location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group, bounds: Default::default(), display: Default::default(), @@ -915,7 +1050,7 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - location: (["/tmp", "/tmp2"]).into(), + location: LocalPaths::new(["/tmp", "/tmp2"]).into(), center_group: Default::default(), bounds: Default::default(), display: Default::default(), @@ -926,7 +1061,7 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - location: (["/tmp"]).into(), + location: LocalPaths::new(["/tmp"]).into(), center_group: Default::default(), bounds: Default::default(), display: Default::default(), @@ -953,7 +1088,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.location = (["/tmp", "/tmp2"]).into(); + workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into(); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -964,7 +1099,7 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - location: (&["/tmp", "/tmp2"]).into(), + location: LocalPaths::new(&["/tmp", "/tmp2"]).into(), center_group: Default::default(), bounds: Default::default(), display: Default::default(), @@ -980,7 +1115,7 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into(); + workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into(); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -999,7 +1134,7 @@ mod tests { ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - location: workspace_id.into(), + location: LocalPaths::new(workspace_id).into(), center_group: center_group.clone(), bounds: Default::default(), display: Default::default(), diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index b8f35447dc..eb64c31c1f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -2,12 +2,14 @@ use super::SerializedAxis; use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; use anyhow::{Context, Result}; use async_recursion::async_recursion; +use client::RemoteProjectId; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView}; use project::Project; +use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -15,59 +17,98 @@ use std::{ use util::ResultExt; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WorkspaceLocation(Arc>); +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SerializedRemoteProject { + pub id: RemoteProjectId, + pub dev_server_name: String, + pub path: String, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPaths(Arc>); + +impl LocalPaths { + pub fn new>(paths: impl IntoIterator) -> Self { + let mut paths: Vec = paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect(); + paths.sort(); + Self(Arc::new(paths)) + } -impl WorkspaceLocation { pub fn paths(&self) -> Arc> { self.0.clone() } +} - #[cfg(any(test, feature = "test-support"))] - pub fn new>(paths: Vec

) -> Self { - Self(Arc::new( - paths - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .collect(), - )) +impl From for SerializedWorkspaceLocation { + fn from(local_paths: LocalPaths) -> Self { + Self::Local(local_paths) } } -impl, T: IntoIterator> From for WorkspaceLocation { - fn from(iterator: T) -> Self { - let mut roots = iterator - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .collect::>(); - roots.sort(); - Self(Arc::new(roots)) - } -} - -impl StaticColumnCount for WorkspaceLocation {} -impl Bind for &WorkspaceLocation { +impl StaticColumnCount for LocalPaths {} +impl Bind for &LocalPaths { fn bind(&self, statement: &Statement, start_index: i32) -> Result { - bincode::serialize(&self.0) - .expect("Bincode serialization of paths should not fail") - .bind(statement, start_index) + statement.bind(&bincode::serialize(&self.0)?, start_index) } } -impl Column for WorkspaceLocation { +impl Column for LocalPaths { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let blob = statement.column_blob(start_index)?; + let path_blob = statement.column_blob(start_index)?; + let paths: Arc> = if path_blob.is_empty() { + Default::default() + } else { + bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? + }; + + Ok((Self(paths), start_index + 1)) + } +} + +impl From for SerializedWorkspaceLocation { + fn from(remote_project: SerializedRemoteProject) -> Self { + Self::Remote(remote_project) + } +} + +impl StaticColumnCount for SerializedRemoteProject {} +impl Bind for &SerializedRemoteProject { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.id.0, start_index)?; + let next_index = statement.bind(&self.dev_server_name, next_index)?; + statement.bind(&self.path, next_index) + } +} + +impl Column for SerializedRemoteProject { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let id = statement.column_int64(start_index)?; + let dev_server_name = statement.column_text(start_index + 1)?.to_string(); + let path = statement.column_text(start_index + 2)?.to_string(); Ok(( - WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?), - start_index + 1, + Self { + id: RemoteProjectId(id as u64), + dev_server_name, + path, + }, + start_index + 3, )) } } +#[derive(Debug, PartialEq, Clone)] +pub enum SerializedWorkspaceLocation { + Local(LocalPaths), + Remote(SerializedRemoteProject), +} + #[derive(Debug, PartialEq, Clone)] pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, - pub(crate) location: WorkspaceLocation, + pub(crate) location: SerializedWorkspaceLocation, pub(crate) center_group: SerializedPaneGroup, pub(crate) bounds: Option>, pub(crate) fullscreen: bool, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5f11c39446..2a6ae60701 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -46,7 +46,7 @@ pub use pane::*; pub use pane_group::*; use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB}; pub use persistence::{ - model::{ItemId, WorkspaceLocation}, + model::{ItemId, LocalPaths, SerializedRemoteProject, SerializedWorkspaceLocation}, WorkspaceDb, DB as WORKSPACE_DB, }; use postage::stream::Stream; @@ -82,7 +82,7 @@ use ui::{ InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, WindowContext, }; -use util::ResultExt; +use util::{maybe, ResultExt}; use uuid::Uuid; pub use workspace_settings::{ AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings, @@ -3392,17 +3392,16 @@ impl Workspace { self.database_id } - fn location(&self, cx: &AppContext) -> Option { + fn local_paths(&self, cx: &AppContext) -> Option { let project = self.project().read(cx); if project.is_local() { - Some( + Some(LocalPaths::new( project .visible_worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() - .into(), - ) + .collect::>(), + )) } else { None } @@ -3540,25 +3539,44 @@ impl Workspace { } } - if let Some(location) = self.location(cx) { - // Load bearing special case: - // - with_local_workspace() relies on this to not have other stuff open - // when you open your log - if !location.paths().is_empty() { - let center_group = build_serialized_pane_group(&self.center.root, cx); - let docks = build_serialized_docks(self, cx); - let serialized_workspace = SerializedWorkspace { - id: self.database_id, - location, - center_group, - bounds: Default::default(), - display: Default::default(), - docks, - fullscreen: cx.is_fullscreen(), - centered_layout: self.centered_layout, - }; - return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); + let location = if let Some(local_paths) = self.local_paths(cx) { + if !local_paths.paths().is_empty() { + Some(SerializedWorkspaceLocation::Local(local_paths)) + } else { + None } + } else if let Some(remote_project_id) = self.project().read(cx).remote_project_id() { + let store = remote_projects::Store::global(cx).read(cx); + maybe!({ + let project = store.remote_project(remote_project_id)?; + let dev_server = store.dev_server(project.dev_server_id)?; + + let remote_project = SerializedRemoteProject { + id: remote_project_id, + dev_server_name: dev_server.name.to_string(), + path: project.path.to_string(), + }; + Some(SerializedWorkspaceLocation::Remote(remote_project)) + }) + } else { + None + }; + + // don't save workspace state for the empty workspace. + if let Some(location) = location { + let center_group = build_serialized_pane_group(&self.center.root, cx); + let docks = build_serialized_docks(self, cx); + let serialized_workspace = SerializedWorkspace { + id: self.database_id, + location, + center_group, + bounds: Default::default(), + display: Default::default(), + docks, + fullscreen: cx.is_fullscreen(), + centered_layout: self.centered_layout, + }; + return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); } Task::ready(()) } @@ -4303,7 +4321,7 @@ pub fn activate_workspace_for_project( None } -pub async fn last_opened_workspace_paths() -> Option { +pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } @@ -4410,7 +4428,6 @@ async fn join_channel_internal( if let Some((project, host)) = room.most_active_project(cx) { return Some(join_in_room_project(project, host, app_state.clone(), cx)); } - // if you are the first to join a channel, share your project if room.remote_participants().len() == 0 && !room.local_participant_is_guest() { if let Some(workspace) = requesting_window { @@ -4419,7 +4436,7 @@ async fn join_channel_internal( return None; } let project = workspace.project.read(cx); - if project.is_local() + if (project.is_local() || project.remote_project_id().is_some()) && project.visible_worktrees(cx).any(|tree| { tree.read(cx) .root_entry() diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8b4ad7134f..d005138dba 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -71,6 +71,7 @@ project_panel.workspace = true project_symbols.workspace = true quick_action_bar.workspace = true recent_projects.workspace = true +remote_projects.workspace = true release_channel.workspace = true rope.workspace = true search.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d591b0525d..3bc06f9ac6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -286,6 +286,7 @@ fn init_ui(args: Args) { ThemeRegistry::global(cx), cx, ); + remote_projects::init(client.clone(), cx); load_user_themes_in_background(fs.clone(), cx); watch_themes(fs.clone(), cx); diff --git a/script/zed-local b/script/zed-local index af15ec8755..0ab6f0d0d1 100755 --- a/script/zed-local +++ b/script/zed-local @@ -42,6 +42,7 @@ let instanceCount = 1; let isReleaseMode = false; let isTop = false; let othersOnStable = false; +let isStateful = false; const args = process.argv.slice(2); while (args.length > 0) { @@ -52,6 +53,8 @@ while (args.length > 0) { instanceCount = parseInt(digitMatch[1]); } else if (arg === "--release") { isReleaseMode = true; + } else if (arg == "--stateful") { + isStateful = true; } else if (arg === "--top") { isTop = true; } else if (arg === "--help") { @@ -147,7 +150,7 @@ setTimeout(() => { env: { ZED_IMPERSONATE: users[i], ZED_WINDOW_POSITION: position, - ZED_STATELESS: "1", + ZED_STATELESS: isStateful && i == 0 ? "1" : "", ZED_ALWAYS_ACTIVE: "1", ZED_SERVER_URL: "http://localhost:3000", ZED_RPC_URL: "http://localhost:8080/rpc", From 68a1ad89bb0d1862711bf97108e5f4296180815e Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Tue, 23 Apr 2024 16:23:26 -0700 Subject: [PATCH 028/101] New revision of the Assistant Panel (#10870) This is a crate only addition of a new version of the AssistantPanel. We'll be putting this behind a feature flag while we iron out the new experience. Release Notes: - N/A --------- Co-authored-by: Nathan Sobo Co-authored-by: Antonio Scandurra Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers Co-authored-by: Antonio Scandurra Co-authored-by: Nate Butler Co-authored-by: Nate Butler Co-authored-by: Max Brunsfeld Co-authored-by: Max --- .zed/tasks.json | 5 + Cargo.lock | 133 ++- Cargo.toml | 5 + assets/keymaps/default-macos.json | 9 +- crates/assets/Cargo.toml | 3 + crates/assets/src/{lib.rs => assets.rs} | 18 +- crates/assistant/src/assistant.rs | 4 + .../src/completion_provider/open_ai.rs | 16 +- .../assistant/src/completion_provider/zed.rs | 2 + crates/assistant2/Cargo.toml | 56 ++ crates/assistant2/LICENSE-GPL | 1 + .../assistant2/examples/assistant_example.rs | 129 +++ crates/assistant2/src/assistant2.rs | 952 ++++++++++++++++++ crates/assistant2/src/assistant_settings.rs | 26 + crates/assistant2/src/completion_provider.rs | 179 ++++ crates/assistant2/src/tools.rs | 176 ++++ crates/assistant_tooling/Cargo.toml | 22 + crates/assistant_tooling/LICENSE-GPL | 1 + crates/assistant_tooling/README.md | 208 ++++ .../src/assistant_tooling.rs | 5 + crates/assistant_tooling/src/registry.rs | 298 ++++++ crates/assistant_tooling/src/tool.rs | 145 +++ crates/client/src/client.rs | 10 + crates/collab/seed.default.json | 3 +- crates/collab/src/ai.rs | 85 +- crates/collab/src/rpc.rs | 90 +- crates/collab_ui/src/chat_panel.rs | 7 +- crates/collab_ui/src/collab_panel.rs | 2 +- .../incoming_call_notification.rs | 2 +- .../project_shared_notification.rs | 2 +- crates/editor/src/editor.rs | 31 +- crates/editor/src/element.rs | 4 +- crates/gpui/src/styled.rs | 26 +- crates/open_ai/src/open_ai.rs | 84 +- crates/project_panel/src/project_panel.rs | 2 +- crates/recent_projects/src/remote_projects.rs | 2 +- crates/rich_text/src/rich_text.rs | 76 +- crates/rpc/proto/zed.proto | 48 + crates/semantic_index/Cargo.toml | 5 + crates/semantic_index/examples/index.rs | 36 +- crates/semantic_index/src/semantic_index.rs | 83 +- crates/settings/src/settings.rs | 9 + crates/storybook/src/story_selector.rs | 4 +- crates/storybook/src/storybook.rs | 11 +- crates/ui/src/components.rs | 2 + .../src/components/collapsible_container.rs | 152 +++ .../title_bar/windows_window_controls.rs | 2 +- crates/ui/src/components/tooltip.rs | 2 +- crates/ui/src/styles/typography.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 28 +- crates/zed/src/zed.rs | 38 +- script/zed-local | 5 +- 55 files changed, 2989 insertions(+), 262 deletions(-) rename crates/assets/src/{lib.rs => assets.rs} (62%) create mode 100644 crates/assistant2/Cargo.toml create mode 120000 crates/assistant2/LICENSE-GPL create mode 100644 crates/assistant2/examples/assistant_example.rs create mode 100644 crates/assistant2/src/assistant2.rs create mode 100644 crates/assistant2/src/assistant_settings.rs create mode 100644 crates/assistant2/src/completion_provider.rs create mode 100644 crates/assistant2/src/tools.rs create mode 100644 crates/assistant_tooling/Cargo.toml create mode 120000 crates/assistant_tooling/LICENSE-GPL create mode 100644 crates/assistant_tooling/README.md create mode 100644 crates/assistant_tooling/src/assistant_tooling.rs create mode 100644 crates/assistant_tooling/src/registry.rs create mode 100644 crates/assistant_tooling/src/tool.rs create mode 100644 crates/ui/src/components/collapsible_container.rs diff --git a/.zed/tasks.json b/.zed/tasks.json index 80465969e2..c95cf5ffb1 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -3,5 +3,10 @@ "label": "clippy", "command": "cargo", "args": ["xtask", "clippy"] + }, + { + "label": "assistant2", + "command": "cargo", + "args": ["run", "-p", "assistant2", "--example", "assistant_example"] } ] diff --git a/Cargo.lock b/Cargo.lock index 3d22a64359..ca625dd461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,50 @@ dependencies = [ "workspace", ] +[[package]] +name = "assistant2" +version = "0.1.0" +dependencies = [ + "anyhow", + "assets", + "assistant_tooling", + "client", + "editor", + "env_logger", + "feature_flags", + "futures 0.3.28", + "gpui", + "language", + "languages", + "log", + "nanoid", + "node_runtime", + "open_ai", + "project", + "release_channel", + "rich_text", + "schemars", + "semantic_index", + "serde", + "serde_json", + "settings", + "theme", + "ui", + "util", + "workspace", +] + +[[package]] +name = "assistant_tooling" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "async-broadcast" version = "0.7.0" @@ -643,7 +687,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -710,7 +754,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -741,7 +785,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -1385,7 +1429,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.48", + "syn 2.0.59", "which 4.4.2", ] @@ -1468,7 +1512,7 @@ source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d2 dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -1634,7 +1678,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -2019,7 +2063,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -2959,7 +3003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" dependencies = [ "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -3442,7 +3486,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -3954,7 +3998,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -4194,7 +4238,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -5061,7 +5105,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -5682,7 +5726,7 @@ checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -6643,7 +6687,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -6719,7 +6763,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -6799,7 +6843,7 @@ checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -6977,7 +7021,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -7028,7 +7072,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -7252,7 +7296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -7309,9 +7353,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -7332,7 +7376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -8175,7 +8219,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.48", + "syn 2.0.59", "walkdir", ] @@ -8449,7 +8493,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -8490,7 +8534,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.48", + "syn 2.0.59", "unicode-ident", ] @@ -8674,7 +8718,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -8739,7 +8783,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -9505,7 +9549,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -9634,9 +9678,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", @@ -10001,7 +10045,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -10180,7 +10224,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -10405,7 +10449,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -11172,7 +11216,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", "wasm-bindgen-shared", ] @@ -11206,7 +11250,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -11343,7 +11387,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", "wasmtime-component-util", "wasmtime-wit-bindgen", "wit-parser", @@ -11504,7 +11548,7 @@ checksum = "6d6d967f01032da7d4c6303da32f6a00d5efe1bac124b156e7342d8ace6ffdfc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -11784,7 +11828,7 @@ dependencies = [ "proc-macro2", "quote", "shellexpand", - "syn 2.0.48", + "syn 2.0.59", "witx", ] @@ -11796,7 +11840,7 @@ checksum = "512d816dbcd0113103b2eb2402ec9018e7f0755202a5b3e67db726f229d8dcae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", "wiggle-generate", ] @@ -11914,7 +11958,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -11925,7 +11969,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -12242,7 +12286,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -12567,6 +12611,7 @@ dependencies = [ "anyhow", "assets", "assistant", + "assistant2", "audio", "auto_update", "backtrace", @@ -12860,7 +12905,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] @@ -12880,7 +12925,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.59", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d2ff0c5066..adb9f461a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "crates/anthropic", "crates/assets", "crates/assistant", + "crates/assistant_tooling", + "crates/assistant2", "crates/audio", "crates/auto_update", "crates/breadcrumbs", @@ -137,6 +139,8 @@ ai = { path = "crates/ai" } anthropic = { path = "crates/anthropic" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } +assistant2 = { path = "crates/assistant2" } +assistant_tooling = { path = "crates/assistant_tooling" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } base64 = "0.13" @@ -208,6 +212,7 @@ rpc = { path = "crates/rpc" } task = { path = "crates/task" } tasks_ui = { path = "crates/tasks_ui" } search = { path = "crates/search" } +semantic_index = { path = "crates/semantic_index" } semantic_version = { path = "crates/semantic_version" } settings = { path = "crates/settings" } snippet = { path = "crates/snippet" } diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f909bd48c5..f4da3078ad 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -209,7 +209,14 @@ } }, { - "context": "AssistantPanel", + "context": "AssistantChat > Editor", // Used in the assistant2 crate + "bindings": { + "enter": ["assistant2::Submit", "Simple"], + "cmd-enter": ["assistant2::Submit", "Codebase"] + } + }, + { + "context": "AssistantPanel", // Used in the assistant crate, which we're replacing "bindings": { "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch" diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml index 8fcb1f9cfe..06f91da59f 100644 --- a/crates/assets/Cargo.toml +++ b/crates/assets/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" publish = false license = "GPL-3.0-or-later" +[lib] +path = "src/assets.rs" + [lints] workspace = true diff --git a/crates/assets/src/lib.rs b/crates/assets/src/assets.rs similarity index 62% rename from crates/assets/src/lib.rs rename to crates/assets/src/assets.rs index 4f013dd5af..b0a32a9d9c 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/assets.rs @@ -1,7 +1,7 @@ // This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build. use anyhow::anyhow; -use gpui::{AssetSource, Result, SharedString}; +use gpui::{AppContext, AssetSource, Result, SharedString}; use rust_embed::RustEmbed; #[derive(RustEmbed)] @@ -34,3 +34,19 @@ impl AssetSource for Assets { .collect()) } } + +impl Assets { + /// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory. + pub fn load_fonts(&self, cx: &AppContext) -> gpui::Result<()> { + let font_paths = self.list("fonts")?; + let mut embedded_fonts = Vec::new(); + for font_path in font_paths { + if font_path.ends_with(".ttf") { + let font_bytes = cx.asset_source().load(&font_path)?; + embedded_fonts.push(font_bytes); + } + } + + cx.text_system().add_fonts(embedded_fonts) + } +} diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 9d72b512a1..46eeb4c095 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -128,6 +128,8 @@ impl LanguageModelRequestMessage { Role::System => proto::LanguageModelRole::LanguageModelSystem, } as i32, content: self.content.clone(), + tool_calls: Vec::new(), + tool_call_id: None, } } } @@ -147,6 +149,8 @@ impl LanguageModelRequest { messages: self.messages.iter().map(|m| m.to_proto()).collect(), stop: self.stop.clone(), temperature: self.temperature, + tool_choice: None, + tools: Vec::new(), } } } diff --git a/crates/assistant/src/completion_provider/open_ai.rs b/crates/assistant/src/completion_provider/open_ai.rs index f4c29a47e8..458a3a9d25 100644 --- a/crates/assistant/src/completion_provider/open_ai.rs +++ b/crates/assistant/src/completion_provider/open_ai.rs @@ -140,14 +140,24 @@ impl OpenAiCompletionProvider { messages: request .messages .into_iter() - .map(|msg| RequestMessage { - role: msg.role.into(), - content: msg.content, + .map(|msg| match msg.role { + Role::User => RequestMessage::User { + content: msg.content, + }, + Role::Assistant => RequestMessage::Assistant { + content: Some(msg.content), + tool_calls: Vec::new(), + }, + Role::System => RequestMessage::System { + content: msg.content, + }, }) .collect(), stream: true, stop: request.stop, temperature: request.temperature, + tools: Vec::new(), + tool_choice: None, } } } diff --git a/crates/assistant/src/completion_provider/zed.rs b/crates/assistant/src/completion_provider/zed.rs index 1ec852da19..ed84f1f7c6 100644 --- a/crates/assistant/src/completion_provider/zed.rs +++ b/crates/assistant/src/completion_provider/zed.rs @@ -123,6 +123,8 @@ impl ZedDotDevCompletionProvider { .collect(), stop: request.stop, temperature: request.temperature, + tools: Vec::new(), + tool_choice: None, }; self.client diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml new file mode 100644 index 0000000000..060dbaa98b --- /dev/null +++ b/crates/assistant2/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "assistant2" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +path = "src/assistant2.rs" + +[[example]] +name = "assistant_example" +path = "examples/assistant_example.rs" +crate-type = ["bin"] + +[dependencies] +anyhow.workspace = true +assistant_tooling.workspace = true +client.workspace = true +editor.workspace = true +feature_flags.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +log.workspace = true +open_ai.workspace = true +project.workspace = true +rich_text.workspace = true +semantic_index.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true +nanoid = "0.4" + +[dev-dependencies] +assets.workspace = true +editor = { workspace = true, features = ["test-support"] } +env_logger.workspace = true +gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +languages.workspace = true +node_runtime.workspace = true +project = { workspace = true, features = ["test-support"] } +release_channel.workspace = true +settings = { workspace = true, features = ["test-support"] } +theme = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } + +[lints] +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/examples/assistant_example.rs b/crates/assistant2/examples/assistant_example.rs new file mode 100644 index 0000000000..260c3bc8f9 --- /dev/null +++ b/crates/assistant2/examples/assistant_example.rs @@ -0,0 +1,129 @@ +use anyhow::Context as _; +use assets::Assets; +use assistant2::{tools::ProjectIndexTool, AssistantPanel}; +use assistant_tooling::ToolRegistry; +use client::Client; +use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions}; +use language::LanguageRegistry; +use project::Project; +use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex}; +use settings::{KeymapFile, DEFAULT_KEYMAP_PATH}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use theme::LoadThemes; +use ui::{div, prelude::*, Render}; +use util::{http::HttpClientWithUrl, ResultExt as _}; + +actions!(example, [Quit]); + +fn main() { + let args: Vec = std::env::args().collect(); + + env_logger::init(); + App::new().with_assets(Assets).run(|cx| { + cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None))); + cx.on_action(|_: &Quit, cx: &mut AppContext| { + cx.quit(); + }); + + if args.len() < 2 { + eprintln!( + "Usage: cargo run --example assistant_example -p assistant2 -- " + ); + cx.quit(); + return; + } + + settings::init(cx); + language::init(cx); + Project::init_settings(cx); + editor::init(cx); + theme::init(LoadThemes::JustBase, cx); + Assets.load_fonts(cx).unwrap(); + KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap(); + client::init_settings(cx); + release_channel::init("0.130.0", cx); + + let client = Client::production(cx); + { + let client = client.clone(); + cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await }) + .detach_and_log_err(cx); + } + assistant2::init(client.clone(), cx); + + let language_registry = Arc::new(LanguageRegistry::new( + Task::ready(()), + cx.background_executor().clone(), + )); + let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client()); + languages::init(language_registry.clone(), node_runtime, cx); + + let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434")); + + let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set"); + let embedding_provider = OpenAiEmbeddingProvider::new( + http.clone(), + OpenAiEmbeddingModel::TextEmbedding3Small, + open_ai::OPEN_AI_API_URL.to_string(), + api_key, + ); + + cx.spawn(|mut cx| async move { + let mut semantic_index = SemanticIndex::new( + PathBuf::from("/tmp/semantic-index-db.mdb"), + Arc::new(embedding_provider), + &mut cx, + ) + .await?; + + let project_path = Path::new(&args[1]); + let project = Project::example([project_path], &mut cx).await; + + cx.update(|cx| { + let fs = project.read(cx).fs().clone(); + + let project_index = semantic_index.project_index(project.clone(), cx); + + let mut tool_registry = ToolRegistry::new(); + tool_registry + .register(ProjectIndexTool::new(project_index.clone(), fs.clone())) + .context("failed to register ProjectIndexTool") + .log_err(); + + let tool_registry = Arc::new(tool_registry); + + cx.open_window(WindowOptions::default(), |cx| { + cx.new_view(|cx| Example::new(language_registry, tool_registry, cx)) + }); + cx.activate(true); + }) + }) + .detach_and_log_err(cx); + }) +} + +struct Example { + assistant_panel: View, +} + +impl Example { + fn new( + language_registry: Arc, + tool_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + Self { + assistant_panel: cx + .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)), + } + } +} + +impl Render for Example { + fn render(&mut self, _cx: &mut ViewContext) -> impl ui::prelude::IntoElement { + div().size_full().child(self.assistant_panel.clone()) + } +} diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs new file mode 100644 index 0000000000..5a9d6c8df6 --- /dev/null +++ b/crates/assistant2/src/assistant2.rs @@ -0,0 +1,952 @@ +mod assistant_settings; +mod completion_provider; +pub mod tools; + +use anyhow::{Context, Result}; +use assistant_tooling::{ToolFunctionCall, ToolRegistry}; +use client::{proto, Client}; +use completion_provider::*; +use editor::{Editor, EditorEvent}; +use feature_flags::FeatureFlagAppExt as _; +use futures::{channel::oneshot, future::join_all, Future, FutureExt, StreamExt}; +use gpui::{ + list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, Global, ListAlignment, ListState, Model, Render, Task, View, WeakView, +}; +use language::{language_settings::SoftWrap, LanguageRegistry}; +use open_ai::{FunctionContent, ToolCall, ToolCallContent}; +use project::Fs; +use rich_text::RichText; +use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex}; +use serde::Deserialize; +use settings::Settings; +use std::{cmp, sync::Arc}; +use theme::ThemeSettings; +use tools::ProjectIndexTool; +use ui::{popover_menu, prelude::*, ButtonLike, CollapsibleContainer, Color, ContextMenu, Tooltip}; +use util::{paths::EMBEDDINGS_DIR, ResultExt}; +use workspace::{ + dock::{DockPosition, Panel, PanelEvent}, + Workspace, +}; + +pub use assistant_settings::AssistantSettings; + +const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5; + +// gpui::actions!(assistant, [Submit]); + +#[derive(Eq, PartialEq, Copy, Clone, Deserialize)] +pub struct Submit(SubmitMode); + +/// There are multiple different ways to submit a model request, represented by this enum. +#[derive(Eq, PartialEq, Copy, Clone, Deserialize)] +pub enum SubmitMode { + /// Only include the conversation. + Simple, + /// Send the current file as context. + CurrentFile, + /// Search the codebase and send relevant excerpts. + Codebase, +} + +gpui::actions!(assistant2, [ToggleFocus]); +gpui::impl_actions!(assistant2, [Submit]); + +pub fn init(client: Arc, cx: &mut AppContext) { + AssistantSettings::register(cx); + + cx.spawn(|mut cx| { + let client = client.clone(); + async move { + let embedding_provider = CloudEmbeddingProvider::new(client.clone()); + let semantic_index = SemanticIndex::new( + EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"), + Arc::new(embedding_provider), + &mut cx, + ) + .await?; + cx.update(|cx| cx.set_global(semantic_index)) + } + }) + .detach(); + + cx.set_global(CompletionProvider::new(CloudCompletionProvider::new( + client, + ))); + + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }, + ) + .detach(); +} + +pub fn enabled(cx: &AppContext) -> bool { + cx.is_staff() +} + +pub struct AssistantPanel { + chat: View, + width: Option, +} + +impl AssistantPanel { + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let (app_state, project) = workspace.update(&mut cx, |workspace, _| { + (workspace.app_state().clone(), workspace.project().clone()) + })?; + + cx.new_view(|cx| { + // todo!("this will panic if the semantic index failed to load or has not loaded yet") + let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| { + semantic_index.project_index(project.clone(), cx) + }); + + let mut tool_registry = ToolRegistry::new(); + tool_registry + .register(ProjectIndexTool::new( + project_index.clone(), + app_state.fs.clone(), + )) + .context("failed to register ProjectIndexTool") + .log_err(); + + let tool_registry = Arc::new(tool_registry); + + Self::new(app_state.languages.clone(), tool_registry, cx) + }) + }) + } + + pub fn new( + language_registry: Arc, + tool_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let chat = cx.new_view(|cx| { + AssistantChat::new(language_registry.clone(), tool_registry.clone(), cx) + }); + + Self { width: None, chat } + } +} + +impl Render for AssistantPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .size_full() + .v_flex() + .p_2() + .bg(cx.theme().colors().background) + .child(self.chat.clone()) + } +} + +impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanelv2" + } + + fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition { + // todo!("Add a setting / use assistant settings") + DockPosition::Right + } + + fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool { + matches!(position, DockPosition::Right) + } + + fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext) { + // Do nothing until we have a setting for this + } + + fn size(&self, _cx: &WindowContext) -> Pixels { + self.width.unwrap_or(px(400.)) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + cx.notify(); + } + + fn icon(&self, _cx: &WindowContext) -> Option { + Some(IconName::Ai) + } + + fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> { + Some("Assistant Panel ✨") + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +impl EventEmitter for AssistantPanel {} + +impl FocusableView for AssistantPanel { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.chat + .read(cx) + .messages + .iter() + .rev() + .find_map(|msg| msg.focus_handle(cx)) + .expect("no user message in chat") + } +} + +struct AssistantChat { + model: String, + messages: Vec, + list_state: ListState, + language_registry: Arc, + next_message_id: MessageId, + pending_completion: Option>, + tool_registry: Arc, +} + +impl AssistantChat { + fn new( + language_registry: Arc, + tool_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let model = CompletionProvider::get(cx).default_model(); + let view = cx.view().downgrade(); + let list_state = ListState::new( + 0, + ListAlignment::Bottom, + px(1024.), + move |ix, cx: &mut WindowContext| { + view.update(cx, |this, cx| this.render_message(ix, cx)) + .unwrap() + }, + ); + + let mut this = Self { + model, + messages: Vec::new(), + list_state, + language_registry, + next_message_id: MessageId(0), + pending_completion: None, + tool_registry, + }; + this.push_new_user_message(true, cx); + this + } + + fn focused_message_id(&self, cx: &WindowContext) -> Option { + self.messages.iter().find_map(|message| match message { + ChatMessage::User(message) => message + .body + .focus_handle(cx) + .contains_focused(cx) + .then_some(message.id), + ChatMessage::Assistant(_) => None, + }) + } + + fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext) { + let Some(focused_message_id) = self.focused_message_id(cx) else { + log::error!("unexpected state: no user message editor is focused."); + return; + }; + + self.truncate_messages(focused_message_id, cx); + + let mode = *mode; + self.pending_completion = Some(cx.spawn(move |this, mut cx| async move { + Self::request_completion( + this.clone(), + mode, + MAX_COMPLETION_CALLS_PER_SUBMISSION, + &mut cx, + ) + .await + .log_err(); + + this.update(&mut cx, |this, cx| { + let focus = this + .user_message(focused_message_id) + .body + .focus_handle(cx) + .contains_focused(cx); + this.push_new_user_message(focus, cx); + }) + .context("Failed to push new user message") + .log_err(); + })); + } + + async fn request_completion( + this: WeakView, + mode: SubmitMode, + limit: usize, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let mut call_count = 0; + loop { + let complete = async { + let completion = this.update(cx, |this, cx| { + this.push_new_assistant_message(cx); + + let definitions = if call_count < limit && matches!(mode, SubmitMode::Codebase) + { + this.tool_registry.definitions() + } else { + &[] + }; + call_count += 1; + + CompletionProvider::get(cx).complete( + this.model.clone(), + this.completion_messages(cx), + Vec::new(), + 1.0, + definitions, + ) + }); + + let mut stream = completion?.await?; + let mut body = String::new(); + while let Some(delta) = stream.next().await { + let delta = delta?; + this.update(cx, |this, cx| { + if let Some(ChatMessage::Assistant(AssistantMessage { + body: message_body, + tool_calls: message_tool_calls, + .. + })) = this.messages.last_mut() + { + if let Some(content) = &delta.content { + body.push_str(content); + } + + for tool_call in delta.tool_calls { + let index = tool_call.index as usize; + if index >= message_tool_calls.len() { + message_tool_calls.resize_with(index + 1, Default::default); + } + let call = &mut message_tool_calls[index]; + + if let Some(id) = &tool_call.id { + call.id.push_str(id); + } + + match tool_call.variant { + Some(proto::tool_call_delta::Variant::Function(tool_call)) => { + if let Some(name) = &tool_call.name { + call.name.push_str(name); + } + if let Some(arguments) = &tool_call.arguments { + call.arguments.push_str(arguments); + } + } + None => {} + } + } + + *message_body = + RichText::new(body.clone(), &[], &this.language_registry); + cx.notify(); + } else { + unreachable!() + } + })?; + } + + anyhow::Ok(()) + } + .await; + + let mut tool_tasks = Vec::new(); + this.update(cx, |this, cx| { + if let Some(ChatMessage::Assistant(AssistantMessage { + error: message_error, + tool_calls, + .. + })) = this.messages.last_mut() + { + if let Err(error) = complete { + message_error.replace(SharedString::from(error.to_string())); + cx.notify(); + } else { + for tool_call in tool_calls.iter() { + tool_tasks.push(this.tool_registry.call(tool_call, cx)); + } + } + } + })?; + + if tool_tasks.is_empty() { + return Ok(()); + } + + let tools = join_all(tool_tasks.into_iter()).await; + this.update(cx, |this, cx| { + if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) = + this.messages.last_mut() + { + *tool_calls = tools; + cx.notify(); + } + })?; + } + } + + fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage { + self.messages + .iter_mut() + .find_map(|message| match message { + ChatMessage::User(user_message) if user_message.id == message_id => { + Some(user_message) + } + _ => None, + }) + .expect("User message not found") + } + + fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext) { + let id = self.next_message_id.post_inc(); + let body = cx.new_view(|cx| { + let mut editor = Editor::auto_height(80, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + if focus { + cx.focus_self(); + } + editor + }); + let _subscription = cx.subscribe(&body, move |this, editor, event, cx| match event { + EditorEvent::SelectionsChanged { .. } => { + if editor.read(cx).is_focused(cx) { + let (message_ix, _message) = this + .messages + .iter() + .enumerate() + .find_map(|(ix, message)| match message { + ChatMessage::User(user_message) if user_message.id == id => { + Some((ix, user_message)) + } + _ => None, + }) + .expect("user message not found"); + + this.list_state.scroll_to_reveal_item(message_ix); + } + } + _ => {} + }); + let message = ChatMessage::User(UserMessage { + id, + body, + contexts: Vec::new(), + _subscription, + }); + self.push_message(message, cx); + } + + fn push_new_assistant_message(&mut self, cx: &mut ViewContext) { + let message = ChatMessage::Assistant(AssistantMessage { + id: self.next_message_id.post_inc(), + body: RichText::default(), + tool_calls: Vec::new(), + error: None, + }); + self.push_message(message, cx); + } + + fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext) { + let old_len = self.messages.len(); + let focus_handle = Some(message.focus_handle(cx)); + self.messages.push(message); + self.list_state + .splice_focusable(old_len..old_len, focus_handle); + cx.notify(); + } + + fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext) { + if let Some(index) = self.messages.iter().position(|message| match message { + ChatMessage::User(message) => message.id == last_message_id, + ChatMessage::Assistant(message) => message.id == last_message_id, + }) { + self.list_state.splice(index + 1..self.messages.len(), 0); + self.messages.truncate(index + 1); + cx.notify(); + } + } + + fn render_error( + &self, + error: Option, + _ix: usize, + cx: &mut ViewContext, + ) -> AnyElement { + let theme = cx.theme(); + + if let Some(error) = error { + div() + .py_1() + .px_2() + .neg_mx_1() + .rounded_md() + .border() + .border_color(theme.status().error_border) + // .bg(theme.status().error_background) + .text_color(theme.status().error) + .child(error.clone()) + .into_any_element() + } else { + div().into_any_element() + } + } + + fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let is_last = ix == self.messages.len() - 1; + + match &self.messages[ix] { + ChatMessage::User(UserMessage { + body, + contexts: _contexts, + .. + }) => div() + .when(!is_last, |element| element.mb_2()) + .child(div().p_2().child(Label::new("You").color(Color::Default))) + .child( + div() + .on_action(cx.listener(Self::submit)) + .p_2() + .text_color(cx.theme().colors().editor_foreground) + .font(ThemeSettings::get_global(cx).buffer_font.clone()) + .bg(cx.theme().colors().editor_background) + .child(body.clone()), // .children(contexts.iter().map(|context| context.render(cx))), + ) + .into_any(), + ChatMessage::Assistant(AssistantMessage { + id, + body, + error, + tool_calls, + .. + }) => { + let assistant_body = if body.text.is_empty() && !tool_calls.is_empty() { + div() + } else { + div().p_2().child(body.element(ElementId::from(id.0), cx)) + }; + + div() + .when(!is_last, |element| element.mb_2()) + .child( + div() + .p_2() + .child(Label::new("Assistant").color(Color::Modified)), + ) + .child(assistant_body) + .child(self.render_error(error.clone(), ix, cx)) + .children(tool_calls.iter().map(|tool_call| { + let result = &tool_call.result; + let name = tool_call.name.clone(); + match result { + Some(result) => div() + .p_2() + .child(result.render(&name, &tool_call.id, cx)) + .into_any(), + None => div() + .p_2() + .child(Label::new(name).color(Color::Modified)) + .child("Running...") + .into_any(), + } + })) + .into_any() + } + } + } + + fn completion_messages(&self, cx: &WindowContext) -> Vec { + let mut completion_messages = Vec::new(); + + for message in &self.messages { + match message { + ChatMessage::User(UserMessage { body, contexts, .. }) => { + // setup context for model + contexts.iter().for_each(|context| { + completion_messages.extend(context.completion_messages(cx)) + }); + + // Show user's message last so that the assistant is grounded in the user's request + completion_messages.push(CompletionMessage::User { + content: body.read(cx).text(cx), + }); + } + ChatMessage::Assistant(AssistantMessage { + body, tool_calls, .. + }) => { + // In no case do we want to send an empty message. This shouldn't happen, but we might as well + // not break the Chat API if it does. + if body.text.is_empty() && tool_calls.is_empty() { + continue; + } + + let tool_calls_from_assistant = tool_calls + .iter() + .map(|tool_call| ToolCall { + content: ToolCallContent::Function { + function: FunctionContent { + name: tool_call.name.clone(), + arguments: tool_call.arguments.clone(), + }, + }, + id: tool_call.id.clone(), + }) + .collect(); + + completion_messages.push(CompletionMessage::Assistant { + content: Some(body.text.to_string()), + tool_calls: tool_calls_from_assistant, + }); + + for tool_call in tool_calls { + // todo!(): we should not be sending when the tool is still running / has no result + // For now I'm going to have to assume we send an empty string because otherwise + // the Chat API will break -- there is a required message for every tool call by ID + let content = match &tool_call.result { + Some(result) => result.format(&tool_call.name), + None => "".to_string(), + }; + + completion_messages.push(CompletionMessage::Tool { + content, + tool_call_id: tool_call.id.clone(), + }); + } + } + } + } + + completion_messages + } + + fn render_model_dropdown(&self, cx: &mut ViewContext) -> impl IntoElement { + let this = cx.view().downgrade(); + div().h_flex().justify_end().child( + div().w_32().child( + popover_menu("user-menu") + .menu(move |cx| { + ContextMenu::build(cx, |mut menu, cx| { + for model in CompletionProvider::get(cx).available_models() { + menu = menu.custom_entry( + { + let model = model.clone(); + move |_| Label::new(model.clone()).into_any_element() + }, + { + let this = this.clone(); + move |cx| { + _ = this.update(cx, |this, cx| { + this.model = model.clone(); + cx.notify(); + }); + } + }, + ); + } + menu + }) + .into() + }) + .trigger( + ButtonLike::new("active-model") + .child( + h_flex() + .w_full() + .gap_0p5() + .child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child(Label::new(self.model.clone())), + ) + .child(div().child( + Icon::new(IconName::ChevronDown).color(Color::Muted), + )), + ) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Change Model", cx)), + ) + .anchor(gpui::AnchorCorner::TopRight), + ), + ) + } +} + +impl Render for AssistantChat { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .relative() + .flex_1() + .v_flex() + .key_context("AssistantChat") + .text_color(Color::Default.color(cx)) + .child(self.render_model_dropdown(cx)) + .child(list(self.list_state.clone()).flex_1()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +struct MessageId(usize); + +impl MessageId { + fn post_inc(&mut self) -> Self { + let id = *self; + self.0 += 1; + id + } +} + +enum ChatMessage { + User(UserMessage), + Assistant(AssistantMessage), +} + +impl ChatMessage { + fn focus_handle(&self, cx: &AppContext) -> Option { + match self { + ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)), + ChatMessage::Assistant(_) => None, + } + } +} + +struct UserMessage { + id: MessageId, + body: View, + contexts: Vec, + _subscription: gpui::Subscription, +} + +struct AssistantMessage { + id: MessageId, + body: RichText, + tool_calls: Vec, + error: Option, +} + +// Since we're swapping out for direct query usage, we might not need to use this injected context +// It will be useful though for when the user _definitely_ wants the model to see a specific file, +// query, error, etc. +#[allow(dead_code)] +enum AssistantContext { + Codebase(View), +} + +#[allow(dead_code)] +struct CodebaseExcerpt { + element_id: ElementId, + path: SharedString, + text: SharedString, + score: f32, + expanded: bool, +} + +impl AssistantContext { + #[allow(dead_code)] + fn render(&self, _cx: &mut ViewContext) -> AnyElement { + match self { + AssistantContext::Codebase(context) => context.clone().into_any_element(), + } + } + + fn completion_messages(&self, cx: &WindowContext) -> Vec { + match self { + AssistantContext::Codebase(context) => context.read(cx).completion_messages(), + } + } +} + +enum CodebaseContext { + Pending { _task: Task<()> }, + Done(Result>), +} + +impl CodebaseContext { + fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext) { + if let CodebaseContext::Done(Ok(excerpts)) = self { + if let Some(excerpt) = excerpts + .iter_mut() + .find(|excerpt| excerpt.element_id == element_id) + { + excerpt.expanded = !excerpt.expanded; + cx.notify(); + } + } + } +} + +impl Render for CodebaseContext { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + match self { + CodebaseContext::Pending { .. } => div() + .h_flex() + .items_center() + .gap_1() + .child(Icon::new(IconName::Ai).color(Color::Muted).into_element()) + .child("Searching codebase..."), + CodebaseContext::Done(Ok(excerpts)) => { + div() + .v_flex() + .gap_2() + .children(excerpts.iter().map(|excerpt| { + let expanded = excerpt.expanded; + let element_id = excerpt.element_id.clone(); + + CollapsibleContainer::new(element_id.clone(), expanded) + .start_slot( + h_flex() + .gap_1() + .child(Icon::new(IconName::File).color(Color::Muted)) + .child(Label::new(excerpt.path.clone()).color(Color::Muted)), + ) + .on_click(cx.listener(move |this, _, cx| { + this.toggle_expanded(element_id.clone(), cx); + })) + .child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .child( + excerpt.text.clone(), // todo!(): Show as an editor block + ), + ) + })) + } + CodebaseContext::Done(Err(error)) => div().child(error.to_string()), + } + } +} + +impl CodebaseContext { + #[allow(dead_code)] + fn new( + query: impl 'static + Future>, + populated: oneshot::Sender, + project_index: Model, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let query = query.boxed_local(); + let _task = cx.spawn(|this, mut cx| async move { + let result = async { + let query = query.await?; + let results = this + .update(&mut cx, |_this, cx| { + project_index.read(cx).search(&query, 16, cx) + })? + .await; + + let excerpts = results.into_iter().map(|result| { + let abs_path = result + .worktree + .read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path)); + let fs = fs.clone(); + + async move { + let path = result.path.clone(); + let text = fs.load(&abs_path?).await?; + // todo!("what should we do with stale ranges?"); + let range = cmp::min(result.range.start, text.len()) + ..cmp::min(result.range.end, text.len()); + + let text = SharedString::from(text[range].to_string()); + + anyhow::Ok(CodebaseExcerpt { + element_id: ElementId::Name(nanoid::nanoid!().into()), + path: path.to_string_lossy().to_string().into(), + text, + score: result.score, + expanded: false, + }) + } + }); + + anyhow::Ok( + futures::future::join_all(excerpts) + .await + .into_iter() + .filter_map(|result| result.log_err()) + .collect(), + ) + } + .await; + + this.update(&mut cx, |this, cx| { + this.populate(result, populated, cx); + }) + .ok(); + }); + + Self::Pending { _task } + } + + #[allow(dead_code)] + fn populate( + &mut self, + result: Result>, + populated: oneshot::Sender, + cx: &mut ViewContext, + ) { + let success = result.is_ok(); + *self = Self::Done(result); + populated.send(success).ok(); + cx.notify(); + } + + fn completion_messages(&self) -> Vec { + // One system message for the whole batch of excerpts: + + // Semantic search results for user query: + // + // Excerpt from $path: + // ~~~ + // `text` + // ~~~ + // + // Excerpt from $path: + + match self { + CodebaseContext::Done(Ok(excerpts)) => { + if excerpts.is_empty() { + return Vec::new(); + } + + let mut body = "Semantic search results for user query:\n".to_string(); + + for excerpt in excerpts { + body.push_str("Excerpt from "); + body.push_str(excerpt.path.as_ref()); + body.push_str(", score "); + body.push_str(&excerpt.score.to_string()); + body.push_str(":\n"); + body.push_str("~~~\n"); + body.push_str(excerpt.text.as_ref()); + body.push_str("~~~\n"); + } + + vec![CompletionMessage::System { content: body }] + } + _ => vec![], + } + } +} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs new file mode 100644 index 0000000000..7d532faaeb --- /dev/null +++ b/crates/assistant2/src/assistant_settings.rs @@ -0,0 +1,26 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +#[derive(Default, Debug, Deserialize, Serialize, Clone)] +pub struct AssistantSettings { + pub enabled: bool, +} + +#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)] +pub struct AssistantSettingsContent { + pub enabled: Option, +} + +impl Settings for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant_v2"); + + type FileContent = AssistantSettingsContent; + + fn load( + sources: SettingsSources, + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Ok(sources.json_merge().unwrap_or_else(|_| Default::default())) + } +} diff --git a/crates/assistant2/src/completion_provider.rs b/crates/assistant2/src/completion_provider.rs new file mode 100644 index 0000000000..01970c053e --- /dev/null +++ b/crates/assistant2/src/completion_provider.rs @@ -0,0 +1,179 @@ +use anyhow::Result; +use assistant_tooling::ToolFunctionDefinition; +use client::{proto, Client}; +use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; +use gpui::Global; +use std::sync::Arc; + +pub use open_ai::RequestMessage as CompletionMessage; + +#[derive(Clone)] +pub struct CompletionProvider(Arc); + +impl CompletionProvider { + pub fn new(backend: impl CompletionProviderBackend) -> Self { + Self(Arc::new(backend)) + } + + pub fn default_model(&self) -> String { + self.0.default_model() + } + + pub fn available_models(&self) -> Vec { + self.0.available_models() + } + + pub fn complete( + &self, + model: String, + messages: Vec, + stop: Vec, + temperature: f32, + tools: &[ToolFunctionDefinition], + ) -> BoxFuture<'static, Result>>> + { + self.0.complete(model, messages, stop, temperature, tools) + } +} + +impl Global for CompletionProvider {} + +pub trait CompletionProviderBackend: 'static { + fn default_model(&self) -> String; + fn available_models(&self) -> Vec; + fn complete( + &self, + model: String, + messages: Vec, + stop: Vec, + temperature: f32, + tools: &[ToolFunctionDefinition], + ) -> BoxFuture<'static, Result>>>; +} + +pub struct CloudCompletionProvider { + client: Arc, +} + +impl CloudCompletionProvider { + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +impl CompletionProviderBackend for CloudCompletionProvider { + fn default_model(&self) -> String { + "gpt-4-turbo".into() + } + + fn available_models(&self) -> Vec { + vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()] + } + + fn complete( + &self, + model: String, + messages: Vec, + stop: Vec, + temperature: f32, + tools: &[ToolFunctionDefinition], + ) -> BoxFuture<'static, Result>>> + { + let client = self.client.clone(); + let tools: Vec = tools + .iter() + .filter_map(|tool| { + Some(proto::ChatCompletionTool { + variant: Some(proto::chat_completion_tool::Variant::Function( + proto::chat_completion_tool::FunctionObject { + name: tool.name.clone(), + description: Some(tool.description.clone()), + parameters: Some(serde_json::to_string(&tool.parameters).ok()?), + }, + )), + }) + }) + .collect(); + + let tool_choice = match tools.is_empty() { + true => None, + false => Some("auto".into()), + }; + + async move { + let stream = client + .request_stream(proto::CompleteWithLanguageModel { + model, + messages: messages + .into_iter() + .map(|message| match message { + CompletionMessage::Assistant { + content, + tool_calls, + } => proto::LanguageModelRequestMessage { + role: proto::LanguageModelRole::LanguageModelAssistant as i32, + content: content.unwrap_or_default(), + tool_call_id: None, + tool_calls: tool_calls + .into_iter() + .map(|tool_call| match tool_call.content { + open_ai::ToolCallContent::Function { function } => { + proto::ToolCall { + id: tool_call.id, + variant: Some(proto::tool_call::Variant::Function( + proto::tool_call::FunctionCall { + name: function.name, + arguments: function.arguments, + }, + )), + } + } + }) + .collect(), + }, + CompletionMessage::User { content } => { + proto::LanguageModelRequestMessage { + role: proto::LanguageModelRole::LanguageModelUser as i32, + content, + tool_call_id: None, + tool_calls: Vec::new(), + } + } + CompletionMessage::System { content } => { + proto::LanguageModelRequestMessage { + role: proto::LanguageModelRole::LanguageModelSystem as i32, + content, + tool_calls: Vec::new(), + tool_call_id: None, + } + } + CompletionMessage::Tool { + content, + tool_call_id, + } => proto::LanguageModelRequestMessage { + role: proto::LanguageModelRole::LanguageModelTool as i32, + content, + tool_call_id: Some(tool_call_id), + tool_calls: Vec::new(), + }, + }) + .collect(), + stop, + temperature, + tool_choice, + tools, + }) + .await?; + + Ok(stream + .filter_map(|response| async move { + match response { + Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)), + Err(error) => Some(Err(error)), + } + }) + .boxed()) + } + .boxed() + } +} diff --git a/crates/assistant2/src/tools.rs b/crates/assistant2/src/tools.rs new file mode 100644 index 0000000000..ffd5e42bfa --- /dev/null +++ b/crates/assistant2/src/tools.rs @@ -0,0 +1,176 @@ +use anyhow::Result; +use assistant_tooling::LanguageModelTool; +use gpui::{prelude::*, AnyElement, AppContext, Model, Task}; +use project::Fs; +use schemars::JsonSchema; +use semantic_index::ProjectIndex; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use ui::{ + div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString, + WindowContext, +}; +use util::ResultExt as _; + +const DEFAULT_SEARCH_LIMIT: usize = 20; + +#[derive(Serialize, Clone)] +pub struct CodebaseExcerpt { + path: SharedString, + text: SharedString, + score: f32, +} + +// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model. +// Any changes or deletions to the `CodebaseQuery` comments will change model behavior. + +#[derive(Deserialize, JsonSchema)] +pub struct CodebaseQuery { + /// Semantic search query + query: String, + /// Maximum number of results to return, defaults to 20 + limit: Option, +} + +pub struct ProjectIndexTool { + project_index: Model, + fs: Arc, +} + +impl ProjectIndexTool { + pub fn new(project_index: Model, fs: Arc) -> Self { + // TODO: setup a better description based on the user's current codebase. + Self { project_index, fs } + } +} + +impl LanguageModelTool for ProjectIndexTool { + type Input = CodebaseQuery; + type Output = Vec; + + fn name(&self) -> String { + "query_codebase".to_string() + } + + fn description(&self) -> String { + "Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string() + } + + fn execute(&self, query: &Self::Input, cx: &AppContext) -> Task> { + let project_index = self.project_index.read(cx); + + let results = project_index.search( + query.query.as_str(), + query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT), + cx, + ); + + let fs = self.fs.clone(); + + cx.spawn(|cx| async move { + let results = results.await; + + let excerpts = results.into_iter().map(|result| { + let abs_path = result + .worktree + .read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path)); + let fs = fs.clone(); + + async move { + let path = result.path.clone(); + let text = fs.load(&abs_path?).await?; + + let mut start = result.range.start; + let mut end = result.range.end.min(text.len()); + while !text.is_char_boundary(start) { + start += 1; + } + while !text.is_char_boundary(end) { + end -= 1; + } + + anyhow::Ok(CodebaseExcerpt { + path: path.to_string_lossy().to_string().into(), + text: SharedString::from(text[start..end].to_string()), + score: result.score, + }) + } + }); + + let excerpts = futures::future::join_all(excerpts) + .await + .into_iter() + .filter_map(|result| result.log_err()) + .collect(); + anyhow::Ok(excerpts) + }) + } + + fn render( + _tool_call_id: &str, + input: &Self::Input, + excerpts: &Self::Output, + cx: &mut WindowContext, + ) -> AnyElement { + let query = input.query.clone(); + + div() + .v_flex() + .gap_2() + .child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .child( + h_flex() + .child(Label::new("Query: ").color(Color::Modified)) + .child(Label::new(query).color(Color::Muted)), + ), + ) + .children(excerpts.iter().map(|excerpt| { + // This render doesn't have state/model, so we can't use the listener + // let expanded = excerpt.expanded; + // let element_id = excerpt.element_id.clone(); + let element_id = ElementId::Name(nanoid::nanoid!().into()); + let expanded = false; + + CollapsibleContainer::new(element_id.clone(), expanded) + .start_slot( + h_flex() + .gap_1() + .child(Icon::new(IconName::File).color(Color::Muted)) + .child(Label::new(excerpt.path.clone()).color(Color::Muted)), + ) + // .on_click(cx.listener(move |this, _, cx| { + // this.toggle_expanded(element_id.clone(), cx); + // })) + .child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .child( + excerpt.text.clone(), // todo!(): Show as an editor block + ), + ) + })) + .into_any_element() + } + + fn format(_input: &Self::Input, excerpts: &Self::Output) -> String { + let mut body = "Semantic search results:\n".to_string(); + + for excerpt in excerpts { + body.push_str("Excerpt from "); + body.push_str(excerpt.path.as_ref()); + body.push_str(", score "); + body.push_str(&excerpt.score.to_string()); + body.push_str(":\n"); + body.push_str("~~~\n"); + body.push_str(excerpt.text.as_ref()); + body.push_str("~~~\n"); + } + body + } +} diff --git a/crates/assistant_tooling/Cargo.toml b/crates/assistant_tooling/Cargo.toml new file mode 100644 index 0000000000..8a7e7ab185 --- /dev/null +++ b/crates/assistant_tooling/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "assistant_tooling" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant_tooling.rs" + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_tooling/LICENSE-GPL b/crates/assistant_tooling/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/assistant_tooling/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_tooling/README.md b/crates/assistant_tooling/README.md new file mode 100644 index 0000000000..79064142ed --- /dev/null +++ b/crates/assistant_tooling/README.md @@ -0,0 +1,208 @@ +# Assistant Tooling + +Bringing OpenAI compatible tool calling to GPUI. + +This unlocks: + +- **Structured Extraction** of model responses +- **Validation** of model inputs +- **Execution** of chosen toolsn + +## Overview + +Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When make a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call. + +> **User**: "Hey I need help with implementing a collapsible panel in GPUI" +> +> **Assistant**: "Sure, I can help with that. Let me see what I can find." +> +> `tool_calls: ["name": "query_codebase", arguments: "{ 'query': 'GPUI collapsible panel' }"]` +> +> `result: "['crates/gpui/src/panel.rs:12: impl Panel { ... }', 'crates/gpui/src/panel.rs:20: impl Panel { ... }']"` +> +> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you." + +This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with a simple trait, `LanguageModelTool`. + +## Example + +Let's expose querying a semantic index directly by the model. First, we'll set up some _necessary_ imports + +```rust +use anyhow::Result; +use assistant_tooling::{LanguageModelTool, ToolRegistry}; +use gpui::{App, AppContext, Task}; +use schemars::JsonSchema; +use serde::Deserialize; +use serde_json::json; +``` + +Then we'll define the query structure the model must fill in. This _must_ derive `Deserialize` from `serde` and `JsonSchema` from the `schemars` crate. + +```rust +#[derive(Deserialize, JsonSchema)] +struct CodebaseQuery { + query: String, +} +``` + +After that we can define our tool, with the expectation that it will need a `ProjectIndex` to search against. For this example, the index uses the same interface as `semantic_index::ProjectIndex`. + +```rust +struct ProjectIndex {} + +impl ProjectIndex { + fn new() -> Self { + ProjectIndex {} + } + + fn search(&self, _query: &str, _limit: usize, _cx: &AppContext) -> Task>> { + // Instead of hooking up a real index, we're going to fake it + if _query.contains("gpui") { + return Task::ready(Ok(vec![r#"// crates/gpui/src/gpui.rs + //! # Welcome to GPUI! + //! + //! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework + //! for Rust, designed to support a wide variety of applications + "# + .to_string()])); + } + return Task::ready(Ok(vec![])); + } +} + +struct ProjectIndexTool { + project_index: ProjectIndex, +} +``` + +Now we can implement the `LanguageModelTool` trait for our tool by: + +- Defining the `Input` from the model, which is `CodebaseQuery` +- Defining the `Output` +- Implementing the `name` and `description` functions to provide the model information when it's choosing a tool +- Implementing the `execute` function to run the tool + +```rust +impl LanguageModelTool for ProjectIndexTool { + type Input = CodebaseQuery; + type Output = String; + + fn name(&self) -> String { + "query_codebase".to_string() + } + + fn description(&self) -> String { + "Executes a query against the codebase, returning excerpts related to the query".to_string() + } + + fn execute(&self, query: Self::Input, cx: &AppContext) -> Task> { + let results = self.project_index.search(query.query.as_str(), 10, cx); + + cx.spawn(|_cx| async move { + let results = results.await?; + + if !results.is_empty() { + Ok(results.join("\n")) + } else { + Ok("No results".to_string()) + } + }) + } +} +``` + +For the sake of this example, let's look at the types that OpenAI will be passing to us + +```rust +// OpenAI definitions, shown here for demonstration +#[derive(Deserialize)] +struct FunctionCall { + name: String, + args: String, +} + +#[derive(Deserialize, Eq, PartialEq)] +enum ToolCallType { + #[serde(rename = "function")] + Function, + Other, +} + +#[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +struct ToolCallId(String); + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum ToolCall { + Function { + #[allow(dead_code)] + id: ToolCallId, + function: FunctionCall, + }, + Other { + #[allow(dead_code)] + id: ToolCallId, + }, +} + +#[derive(Deserialize)] +struct AssistantMessage { + role: String, + content: Option, + tool_calls: Option>, +} +``` + +When the model wants to call tools, it will pass a list of `ToolCall`s. When those are `function`s that we can handle, we'll pass them to our `ToolRegistry` to get a future that we can await. + +```rust +// Inside `fn main()` +App::new().run(|cx: &mut AppContext| { + let tool = ProjectIndexTool { + project_index: ProjectIndex::new(), + }; + + let mut registry = ToolRegistry::new(); + let registered = registry.register(tool); + assert!(registered.is_ok()); +``` + +Let's pretend the model sent us back a message requesting + +```rust +let model_response = json!({ + "role": "assistant", + "tool_calls": [ + { + "id": "call_1", + "function": { + "name": "query_codebase", + "args": r#"{"query":"GPUI Task background_executor"}"# + }, + "type": "function" + } + ] +}); + +let message: AssistantMessage = serde_json::from_value(model_response).unwrap(); + +// We know there's a tool call, so let's skip straight to it for this example +let tool_calls = message.tool_calls.as_ref().unwrap(); +let tool_call = tool_calls.get(0).unwrap(); +``` + +We can now use our registry to call the tool. + +```rust +let task = registry.call( + tool_call.name, + tool_call.args, +); + +cx.spawn(|_cx| async move { + let result = task.await?; + println!("{}", result.unwrap()); + Ok(()) +}) +``` diff --git a/crates/assistant_tooling/src/assistant_tooling.rs b/crates/assistant_tooling/src/assistant_tooling.rs new file mode 100644 index 0000000000..93d81cbb9d --- /dev/null +++ b/crates/assistant_tooling/src/assistant_tooling.rs @@ -0,0 +1,5 @@ +pub mod registry; +pub mod tool; + +pub use crate::registry::ToolRegistry; +pub use crate::tool::{LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition}; diff --git a/crates/assistant_tooling/src/registry.rs b/crates/assistant_tooling/src/registry.rs new file mode 100644 index 0000000000..8c969c0d80 --- /dev/null +++ b/crates/assistant_tooling/src/registry.rs @@ -0,0 +1,298 @@ +use anyhow::{anyhow, Result}; +use gpui::{AnyElement, AppContext, Task, WindowContext}; +use std::{any::Any, collections::HashMap}; + +use crate::tool::{ + LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition, +}; + +pub struct ToolRegistry { + tools: HashMap Task>>, + definitions: Vec, +} + +impl ToolRegistry { + pub fn new() -> Self { + Self { + tools: HashMap::new(), + definitions: Vec::new(), + } + } + + pub fn definitions(&self) -> &[ToolFunctionDefinition] { + &self.definitions + } + + pub fn register(&mut self, tool: T) -> Result<()> { + fn render( + tool_call_id: &str, + input: &Box, + output: &Box, + cx: &mut WindowContext, + ) -> AnyElement { + T::render( + tool_call_id, + input.as_ref().downcast_ref::().unwrap(), + output.as_ref().downcast_ref::().unwrap(), + cx, + ) + } + + fn format( + input: &Box, + output: &Box, + ) -> String { + T::format( + input.as_ref().downcast_ref::().unwrap(), + output.as_ref().downcast_ref::().unwrap(), + ) + } + + self.definitions.push(tool.definition()); + let name = tool.name(); + let previous = self.tools.insert( + name.clone(), + Box::new(move |tool_call: &ToolFunctionCall, cx: &AppContext| { + let name = tool_call.name.clone(); + let arguments = tool_call.arguments.clone(); + let id = tool_call.id.clone(); + + let Ok(input) = serde_json::from_str::(arguments.as_str()) else { + return Task::ready(ToolFunctionCall { + id, + name: name.clone(), + arguments, + result: Some(ToolFunctionCallResult::ParsingFailed), + }); + }; + + let result = tool.execute(&input, cx); + + cx.spawn(move |_cx| async move { + match result.await { + Ok(result) => { + let result: T::Output = result; + ToolFunctionCall { + id, + name: name.clone(), + arguments, + result: Some(ToolFunctionCallResult::Finished { + input: Box::new(input), + output: Box::new(result), + render_fn: render::, + format_fn: format::, + }), + } + } + Err(_error) => ToolFunctionCall { + id, + name: name.clone(), + arguments, + result: Some(ToolFunctionCallResult::ExecutionFailed { + input: Box::new(input), + }), + }, + } + }) + }), + ); + + if previous.is_some() { + return Err(anyhow!("already registered a tool with name {}", name)); + } + + Ok(()) + } + + pub fn call(&self, tool_call: &ToolFunctionCall, cx: &AppContext) -> Task { + let name = tool_call.name.clone(); + let arguments = tool_call.arguments.clone(); + let id = tool_call.id.clone(); + + let tool = match self.tools.get(&name) { + Some(tool) => tool, + None => { + let name = name.clone(); + return Task::ready(ToolFunctionCall { + id, + name: name.clone(), + arguments, + result: Some(ToolFunctionCallResult::NoSuchTool), + }); + } + }; + + tool(tool_call, cx) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use schemars::schema_for; + + use gpui::{div, AnyElement, Element, ParentElement, TestAppContext, WindowContext}; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use serde_json::json; + + #[derive(Deserialize, Serialize, JsonSchema)] + struct WeatherQuery { + location: String, + unit: String, + } + + struct WeatherTool { + current_weather: WeatherResult, + } + + #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] + struct WeatherResult { + location: String, + temperature: f64, + unit: String, + } + + impl LanguageModelTool for WeatherTool { + type Input = WeatherQuery; + type Output = WeatherResult; + + fn name(&self) -> String { + "get_current_weather".to_string() + } + + fn description(&self) -> String { + "Fetches the current weather for a given location.".to_string() + } + + fn execute(&self, input: &WeatherQuery, _cx: &AppContext) -> Task> { + let _location = input.location.clone(); + let _unit = input.unit.clone(); + + let weather = self.current_weather.clone(); + + Task::ready(Ok(weather)) + } + + fn render( + _tool_call_id: &str, + _input: &Self::Input, + output: &Self::Output, + _cx: &mut WindowContext, + ) -> AnyElement { + div() + .child(format!( + "The current temperature in {} is {} {}", + output.location, output.temperature, output.unit + )) + .into_any() + } + + fn format(_input: &Self::Input, output: &Self::Output) -> String { + format!( + "The current temperature in {} is {} {}", + output.location, output.temperature, output.unit + ) + } + } + + #[gpui::test] + async fn test_function_registry(cx: &mut TestAppContext) { + cx.background_executor.run_until_parked(); + + let mut registry = ToolRegistry::new(); + + let tool = WeatherTool { + current_weather: WeatherResult { + location: "San Francisco".to_string(), + temperature: 21.0, + unit: "Celsius".to_string(), + }, + }; + + registry.register(tool).unwrap(); + + let _result = cx + .update(|cx| { + registry.call( + &ToolFunctionCall { + name: "get_current_weather".to_string(), + arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"# + .to_string(), + id: "test-123".to_string(), + result: None, + }, + cx, + ) + }) + .await; + + // assert!(result.is_ok()); + // let result = result.unwrap(); + + // let expected = r#"{"location":"San Francisco","temperature":21.0,"unit":"Celsius"}"#; + + // todo!(): Put this back in after the interface is stabilized + // assert_eq!(result, expected); + } + + #[gpui::test] + async fn test_openai_weather_example(cx: &mut TestAppContext) { + cx.background_executor.run_until_parked(); + + let tool = WeatherTool { + current_weather: WeatherResult { + location: "San Francisco".to_string(), + temperature: 21.0, + unit: "Celsius".to_string(), + }, + }; + + let tools = vec![tool.definition()]; + assert_eq!(tools.len(), 1); + + let expected = ToolFunctionDefinition { + name: "get_current_weather".to_string(), + description: "Fetches the current weather for a given location.".to_string(), + parameters: schema_for!(WeatherQuery).schema, + }; + + assert_eq!(tools[0].name, expected.name); + assert_eq!(tools[0].description, expected.description); + + let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap(); + + assert_eq!( + expected_schema, + json!({ + "title": "WeatherQuery", + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "unit": { + "type": "string" + } + }, + "required": ["location", "unit"] + }) + ); + + let args = json!({ + "location": "San Francisco", + "unit": "Celsius" + }); + + let query: WeatherQuery = serde_json::from_value(args).unwrap(); + + let result = cx.update(|cx| tool.execute(&query, cx)).await; + + assert!(result.is_ok()); + let result = result.unwrap(); + + assert_eq!(result, tool.current_weather); + } +} diff --git a/crates/assistant_tooling/src/tool.rs b/crates/assistant_tooling/src/tool.rs new file mode 100644 index 0000000000..b63e2901c6 --- /dev/null +++ b/crates/assistant_tooling/src/tool.rs @@ -0,0 +1,145 @@ +use anyhow::Result; +use gpui::{div, AnyElement, AppContext, Element, ParentElement as _, Task, WindowContext}; +use schemars::{schema::SchemaObject, schema_for, JsonSchema}; +use serde::Deserialize; +use std::{any::Any, fmt::Debug}; + +#[derive(Default, Deserialize)] +pub struct ToolFunctionCall { + pub id: String, + pub name: String, + pub arguments: String, + #[serde(skip)] + pub result: Option, +} + +pub enum ToolFunctionCallResult { + NoSuchTool, + ParsingFailed, + ExecutionFailed { + input: Box, + }, + Finished { + input: Box, + output: Box, + render_fn: fn( + // tool_call_id + &str, + // LanguageModelTool::Input + &Box, + // LanguageModelTool::Output + &Box, + &mut WindowContext, + ) -> AnyElement, + format_fn: fn( + // LanguageModelTool::Input + &Box, + // LanguageModelTool::Output + &Box, + ) -> String, + }, +} + +impl ToolFunctionCallResult { + pub fn render( + &self, + tool_name: &str, + tool_call_id: &str, + cx: &mut WindowContext, + ) -> AnyElement { + match self { + ToolFunctionCallResult::NoSuchTool => { + div().child(format!("no such tool {tool_name}")).into_any() + } + ToolFunctionCallResult::ParsingFailed => div() + .child(format!("failed to parse input for tool {tool_name}")) + .into_any(), + ToolFunctionCallResult::ExecutionFailed { .. } => div() + .child(format!("failed to execute tool {tool_name}")) + .into_any(), + ToolFunctionCallResult::Finished { + input, + output, + render_fn, + .. + } => render_fn(tool_call_id, input, output, cx), + } + } + + pub fn format(&self, tool: &str) -> String { + match self { + ToolFunctionCallResult::NoSuchTool => format!("no such tool {tool}"), + ToolFunctionCallResult::ParsingFailed => { + format!("failed to parse input for tool {tool}") + } + ToolFunctionCallResult::ExecutionFailed { input: _input } => { + format!("failed to execute tool {tool}") + } + ToolFunctionCallResult::Finished { + input, + output, + format_fn, + .. + } => format_fn(input, output), + } + } +} + +#[derive(Clone)] +pub struct ToolFunctionDefinition { + pub name: String, + pub description: String, + pub parameters: SchemaObject, +} + +impl Debug for ToolFunctionDefinition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let schema = serde_json::to_string(&self.parameters).ok(); + let schema = schema.unwrap_or("None".to_string()); + + f.debug_struct("ToolFunctionDefinition") + .field("name", &self.name) + .field("description", &self.description) + .field("parameters", &schema) + .finish() + } +} + +pub trait LanguageModelTool { + /// The input type that will be passed in to `execute` when the tool is called + /// by the language model. + type Input: for<'de> Deserialize<'de> + JsonSchema; + + /// The output returned by executing the tool. + type Output: 'static; + + /// The name of the tool is exposed to the language model to allow + /// the model to pick which tools to use. As this name is used to + /// identify the tool within a tool registry, it should be unique. + fn name(&self) -> String; + + /// A description of the tool that can be used to _prompt_ the model + /// as to what the tool does. + fn description(&self) -> String; + + /// The OpenAI Function definition for the tool, for direct use with OpenAI's API. + fn definition(&self) -> ToolFunctionDefinition { + ToolFunctionDefinition { + name: self.name(), + description: self.description(), + parameters: schema_for!(Self::Input).schema, + } + } + + /// Execute the tool + fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task>; + + fn render( + tool_call_id: &str, + input: &Self::Input, + output: &Self::Output, + cx: &mut WindowContext, + ) -> AnyElement; + + fn format(input: &Self::Input, output: &Self::Output) -> String; +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 7d18e5d2db..7787089568 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -457,6 +457,14 @@ impl Client { }) } + pub fn production(cx: &mut AppContext) -> Arc { + let clock = Arc::new(clock::RealSystemClock); + let http = Arc::new(HttpClientWithUrl::new( + &ClientSettings::get_global(cx).server_url, + )); + Self::new(clock, http.clone(), cx) + } + pub fn id(&self) -> u64 { self.id.load(Ordering::SeqCst) } @@ -1119,6 +1127,8 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { + eprintln!("authenticate as admin {login}, {token}"); + return Self::authenticate_as_admin(http, login.clone(), token.clone()) .await; } diff --git a/crates/collab/seed.default.json b/crates/collab/seed.default.json index ded1dc862b..1abec644be 100644 --- a/crates/collab/seed.default.json +++ b/crates/collab/seed.default.json @@ -5,7 +5,8 @@ "maxbrunsfeld", "iamnbutler", "mikayla-maki", - "JosephTLyons" + "JosephTLyons", + "rgbkrk" ], "channels": ["zed"], "number_of_users": 100 diff --git a/crates/collab/src/ai.rs b/crates/collab/src/ai.rs index 4634166799..06c6e77dfd 100644 --- a/crates/collab/src/ai.rs +++ b/crates/collab/src/ai.rs @@ -1,5 +1,6 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; use rpc::proto; +use util::ResultExt as _; pub fn language_model_request_to_open_ai( request: proto::CompleteWithLanguageModel, @@ -9,24 +10,83 @@ pub fn language_model_request_to_open_ai( messages: request .messages .into_iter() - .map(|message| { + .map(|message: proto::LanguageModelRequestMessage| { let role = proto::LanguageModelRole::from_i32(message.role) .ok_or_else(|| anyhow!("invalid role {}", message.role))?; - Ok(open_ai::RequestMessage { - role: match role { - proto::LanguageModelRole::LanguageModelUser => open_ai::Role::User, - proto::LanguageModelRole::LanguageModelAssistant => { - open_ai::Role::Assistant - } - proto::LanguageModelRole::LanguageModelSystem => open_ai::Role::System, + + let openai_message = match role { + proto::LanguageModelRole::LanguageModelUser => open_ai::RequestMessage::User { + content: message.content, }, - content: message.content, - }) + proto::LanguageModelRole::LanguageModelAssistant => { + open_ai::RequestMessage::Assistant { + content: Some(message.content), + tool_calls: message + .tool_calls + .into_iter() + .filter_map(|call| { + Some(open_ai::ToolCall { + id: call.id, + content: match call.variant? { + proto::tool_call::Variant::Function(f) => { + open_ai::ToolCallContent::Function { + function: open_ai::FunctionContent { + name: f.name, + arguments: f.arguments, + }, + } + } + }, + }) + }) + .collect(), + } + } + proto::LanguageModelRole::LanguageModelSystem => { + open_ai::RequestMessage::System { + content: message.content, + } + } + proto::LanguageModelRole::LanguageModelTool => open_ai::RequestMessage::Tool { + tool_call_id: message + .tool_call_id + .ok_or_else(|| anyhow!("tool message is missing tool call id"))?, + content: message.content, + }, + }; + + Ok(openai_message) }) .collect::>>()?, stream: true, stop: request.stop, temperature: request.temperature, + tools: request + .tools + .into_iter() + .filter_map(|tool| { + Some(match tool.variant? { + proto::chat_completion_tool::Variant::Function(f) => { + open_ai::ToolDefinition::Function { + function: open_ai::FunctionDefinition { + name: f.name, + description: f.description, + parameters: if let Some(params) = &f.parameters { + Some( + serde_json::from_str(params) + .context("failed to deserialize tool parameters") + .log_err()?, + ) + } else { + None + }, + }, + } + } + }) + }) + .collect(), + tool_choice: request.tool_choice, }) } @@ -58,6 +118,9 @@ pub fn language_model_request_message_to_google_ai( proto::LanguageModelRole::LanguageModelUser => google_ai::Role::User, proto::LanguageModelRole::LanguageModelAssistant => google_ai::Role::Model, proto::LanguageModelRole::LanguageModelSystem => google_ai::Role::User, + proto::LanguageModelRole::LanguageModelTool => { + Err(anyhow!("we don't handle tool calls with google ai yet"))? + } }, }) } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 3cba88b543..b2588e6fb3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -775,9 +775,7 @@ impl Server { Box::new(move |envelope, session| { let envelope = envelope.into_any().downcast::>().unwrap(); let received_at = envelope.received_at; - tracing::info!( - "message received" - ); + tracing::info!("message received"); let start_time = Instant::now(); let future = (handler)(*envelope, session); async move { @@ -786,12 +784,24 @@ impl Server { let processing_duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0; let queue_duration_ms = total_duration_ms - processing_duration_ms; let payload_type = M::NAME; + match result { Err(error) => { - // todo!(), why isn't this logged inside the span? - tracing::error!(%error, total_duration_ms, processing_duration_ms, queue_duration_ms, payload_type, "error handling message") + tracing::error!( + ?error, + total_duration_ms, + processing_duration_ms, + queue_duration_ms, + payload_type, + "error handling message" + ) } - Ok(()) => tracing::info!(total_duration_ms, processing_duration_ms, queue_duration_ms, "finished handling message"), + Ok(()) => tracing::info!( + total_duration_ms, + processing_duration_ms, + queue_duration_ms, + "finished handling message" + ), } } .boxed() @@ -4098,7 +4108,7 @@ async fn complete_with_open_ai( crate::ai::language_model_request_to_open_ai(request)?, ) .await - .context("open_ai::stream_completion request failed")?; + .context("open_ai::stream_completion request failed within collab")?; while let Some(event) = completion_stream.next().await { let event = event?; @@ -4113,8 +4123,32 @@ async fn complete_with_open_ai( open_ai::Role::User => LanguageModelRole::LanguageModelUser, open_ai::Role::Assistant => LanguageModelRole::LanguageModelAssistant, open_ai::Role::System => LanguageModelRole::LanguageModelSystem, + open_ai::Role::Tool => LanguageModelRole::LanguageModelTool, } as i32), content: choice.delta.content, + tool_calls: choice + .delta + .tool_calls + .into_iter() + .map(|delta| proto::ToolCallDelta { + index: delta.index as u32, + id: delta.id, + variant: match delta.function { + Some(function) => { + let name = function.name; + let arguments = function.arguments; + + Some(proto::tool_call_delta::Variant::Function( + proto::tool_call_delta::FunctionCallDelta { + name, + arguments, + }, + )) + } + None => None, + }, + }) + .collect(), }), finish_reason: choice.finish_reason, }) @@ -4165,6 +4199,8 @@ async fn complete_with_google_ai( }) .collect(), ), + // Tool calls are not supported for Google + tool_calls: Vec::new(), }), finish_reason: candidate.finish_reason.map(|reason| reason.to_string()), }) @@ -4187,24 +4223,28 @@ async fn complete_with_anthropic( let messages = request .messages .into_iter() - .filter_map(|message| match message.role() { - LanguageModelRole::LanguageModelUser => Some(anthropic::RequestMessage { - role: anthropic::Role::User, - content: message.content, - }), - LanguageModelRole::LanguageModelAssistant => Some(anthropic::RequestMessage { - role: anthropic::Role::Assistant, - content: message.content, - }), - // Anthropic's API breaks system instructions out as a separate field rather - // than having a system message role. - LanguageModelRole::LanguageModelSystem => { - if !system_message.is_empty() { - system_message.push_str("\n\n"); - } - system_message.push_str(&message.content); + .filter_map(|message| { + match message.role() { + LanguageModelRole::LanguageModelUser => Some(anthropic::RequestMessage { + role: anthropic::Role::User, + content: message.content, + }), + LanguageModelRole::LanguageModelAssistant => Some(anthropic::RequestMessage { + role: anthropic::Role::Assistant, + content: message.content, + }), + // Anthropic's API breaks system instructions out as a separate field rather + // than having a system message role. + LanguageModelRole::LanguageModelSystem => { + if !system_message.is_empty() { + system_message.push_str("\n\n"); + } + system_message.push_str(&message.content); - None + None + } + // We don't yet support tool calls for Anthropic + LanguageModelRole::LanguageModelTool => None, } }) .collect(); @@ -4248,6 +4288,7 @@ async fn complete_with_anthropic( delta: Some(proto::LanguageModelResponseMessage { role: Some(current_role as i32), content: Some(text), + tool_calls: Vec::new(), }), finish_reason: None, }], @@ -4264,6 +4305,7 @@ async fn complete_with_anthropic( delta: Some(proto::LanguageModelResponseMessage { role: Some(current_role as i32), content: Some(text), + tool_calls: Vec::new(), }), finish_reason: None, }], diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 58384f5ee5..ef37ce653b 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -234,10 +234,11 @@ impl ChatPanel { let channel_id = chat.read(cx).channel_id; { self.markdown_data.clear(); - let chat = chat.read(cx); - self.message_list.reset(chat.message_count()); + let chat = chat.read(cx); let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); + let message_count = chat.message_count(); + self.message_list.reset(message_count); self.message_editor.update(cx, |editor, cx| { editor.set_channel(channel_id, channel_name, cx); editor.clear_reply_to_message_id(); @@ -766,7 +767,7 @@ impl ChatPanel { body.push_str(MESSAGE_EDITED); } - let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None); + let mut rich_text = RichText::new(body, &mentions, language_registry); if message.edited_at.is_some() { let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8b5eed08d9..d9b3f1abbf 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2947,7 +2947,7 @@ impl Render for DraggedChannelView { fn render(&mut self, cx: &mut ViewContext) -> impl Element { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); h_flex() - .font(ui_font) + .font_family(ui_font) .bg(cx.theme().colors().background) .w(self.width) .p_1() diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index a8ba20c1e5..385e903bf7 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -125,7 +125,7 @@ impl Render for IncomingCallNotification { cx.set_rem_size(ui_font_size); - div().size_full().font(ui_font).child( + div().size_full().font_family(ui_font).child( CollabNotification::new( self.state.call.calling_user.avatar_uri.clone(), Button::new("accept", "Accept").on_click({ diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 407ff66d19..03001bc3ad 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -129,7 +129,7 @@ impl Render for ProjectSharedNotification { cx.set_rem_size(ui_font_size); - div().size_full().font(ui_font).child( + div().size_full().font_family(ui_font).child( CollabNotification::new( self.owner.avatar_uri.clone(), Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7dc3caed7..cfca895eff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -61,13 +61,13 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use git::blame::GitBlame; use git::diff_hunk_to_display; use gpui::{ - div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, - AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, - ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, - FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, - MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, - Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, - View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext, + div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, + AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, + Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle, + FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad, + ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText, + Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext, + ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -8885,7 +8885,6 @@ impl Editor { self.style = Some(style); } - #[cfg(any(test, feature = "test-support"))] pub fn style(&self) -> Option<&EditorStyle> { self.style.as_ref() } @@ -10322,21 +10321,9 @@ impl FocusableView for Editor { impl Render for Editor { fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); - let text_style = match self.mode { - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(settings.buffer_line_height.value()), - background_color: None, - underline: None, - strikethrough: None, - white_space: WhiteSpace::Normal, - }, + let text_style = match self.mode { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => cx.text_style(), EditorMode::Full => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.buffer_font.family.clone(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b9fed082fc..49917d7ade 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3056,7 +3056,7 @@ fn render_inline_blame_entry( h_flex() .id("inline-blame") .w_full() - .font(style.text.font().family) + .font_family(style.text.font().family) .text_color(cx.theme().status().hint) .line_height(style.text.line_height) .child(Icon::new(IconName::FileGit).color(Color::Hint)) @@ -3108,7 +3108,7 @@ fn render_blame_entry( h_flex() .w_full() - .font(style.text.font().family) + .font_family(style.text.font().family) .line_height(style.text.line_height) .id(("blame", ix)) .children([ diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 54adbb3891..9705f4dd13 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,7 +1,7 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, - DefiniteLength, Fill, FlexDirection, FlexWrap, FontStyle, FontWeight, Hsla, JustifyContent, - Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace, + DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, + JustifyContent, Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; @@ -771,14 +771,32 @@ pub trait Styled: Sized { self } - /// Change the font on this element and its children. - fn font(mut self, family_name: impl Into) -> Self { + /// Change the font family on this element and its children. + fn font_family(mut self, family_name: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_family = Some(family_name.into()); self } + /// Change the font of this element and its children. + fn font(mut self, font: Font) -> Self { + let Font { + family, + features, + weight, + style, + } = font; + + let text_style = self.text_style().get_or_insert_with(Default::default); + text_style.font_family = Some(family); + text_style.font_features = Some(features); + text_style.font_weight = Some(weight); + text_style.font_style = Some(style); + + self + } + /// Set the line height on this element and its children. fn line_height(mut self, line_height: impl Into) -> Self { self.text_style() diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 97abb45dfc..bdc6d3cb9b 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Context, Result}; use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt}; use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; use std::{convert::TryFrom, future::Future}; use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest}; @@ -12,6 +13,7 @@ pub enum Role { User, Assistant, System, + Tool, } impl TryFrom for Role { @@ -22,6 +24,7 @@ impl TryFrom for Role { "user" => Ok(Self::User), "assistant" => Ok(Self::Assistant), "system" => Ok(Self::System), + "tool" => Ok(Self::Tool), _ => Err(anyhow!("invalid role '{value}'")), } } @@ -33,6 +36,7 @@ impl From for String { Role::User => "user".to_owned(), Role::Assistant => "assistant".to_owned(), Role::System => "system".to_owned(), + Role::Tool => "tool".to_owned(), } } } @@ -91,18 +95,88 @@ pub struct Request { pub stream: bool, pub stop: Vec, pub temperature: f32, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub tools: Vec, +} + +#[derive(Debug, Serialize)] +pub struct FunctionDefinition { + pub name: String, + pub description: Option, + pub parameters: Option>, +} + +#[derive(Serialize, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ToolDefinition { + #[allow(dead_code)] + Function { function: FunctionDefinition }, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct RequestMessage { - pub role: Role, - pub content: String, +#[serde(tag = "role", rename_all = "lowercase")] +pub enum RequestMessage { + Assistant { + content: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + tool_calls: Vec, + }, + User { + content: String, + }, + System { + content: String, + }, + Tool { + content: String, + tool_call_id: String, + }, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessage { +pub struct ToolCall { + pub id: String, + #[serde(flatten)] + pub content: ToolCallContent, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ToolCallContent { + Function { function: FunctionContent }, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct FunctionContent { + pub name: String, + pub arguments: String, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ResponseMessageDelta { pub role: Option, pub content: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tool_calls: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ToolCallChunk { + pub index: usize, + pub id: Option, + + // There is also an optional `type` field that would determine if a + // function is there. Sometimes this streams in with the `function` before + // it streams in the `type` + pub function: Option, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct FunctionChunk { + pub name: Option, + pub arguments: Option, } #[derive(Deserialize, Debug)] @@ -115,7 +189,7 @@ pub struct Usage { #[derive(Deserialize, Debug)] pub struct ChoiceDelta { pub index: u32, - pub delta: ResponseMessage, + pub delta: ResponseMessageDelta, pub finish_reason: Option, } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f592103b20..ddbd8429ff 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1843,7 +1843,7 @@ impl Render for DraggedProjectEntryView { let settings = ProjectPanelSettings::get_global(cx); let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); h_flex() - .font(ui_font) + .font_family(ui_font) .bg(cx.theme().colors().background) .w(self.width) .child( diff --git a/crates/recent_projects/src/remote_projects.rs b/crates/recent_projects/src/remote_projects.rs index 2a2f13d945..8a548c747e 100644 --- a/crates/recent_projects/src/remote_projects.rs +++ b/crates/recent_projects/src/remote_projects.rs @@ -507,7 +507,7 @@ impl RemoteProjects { .my_1() .py_0p5() .px_3() - .font(ThemeSettings::get_global(cx).buffer_font.family.clone()) + .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone()) .child(Label::new(instructions)) ) .when(status == DevServerStatus::Offline, |this| { diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 78dabe0ca3..16c4473e07 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -43,6 +43,19 @@ pub struct RichText { Option, &mut WindowContext) -> Option>>, } +impl Default for RichText { + fn default() -> Self { + Self { + text: SharedString::default(), + highlights: Vec::new(), + link_ranges: Vec::new(), + link_urls: Arc::from([]), + custom_ranges: Vec::new(), + custom_ranges_tooltip_fn: None, + } + } +} + /// Allows one to specify extra links to the rendered markdown, which can be used /// for e.g. mentions. #[derive(Debug)] @@ -52,6 +65,37 @@ pub struct Mention { } impl RichText { + pub fn new( + block: String, + mentions: &[Mention], + language_registry: &Arc, + ) -> Self { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut link_ranges = Vec::new(); + let mut link_urls = Vec::new(); + render_markdown_mut( + &block, + mentions, + language_registry, + None, + &mut text, + &mut highlights, + &mut link_ranges, + &mut link_urls, + ); + text.truncate(text.trim_end().len()); + + RichText { + text: SharedString::from(text), + link_urls: link_urls.into(), + link_ranges, + highlights, + custom_ranges: Vec::new(), + custom_ranges_tooltip_fn: None, + } + } + pub fn set_tooltip_builder_for_custom_ranges( &mut self, f: impl Fn(usize, Range, &mut WindowContext) -> Option + 'static, @@ -347,38 +391,6 @@ pub fn render_markdown_mut( } } -pub fn render_rich_text( - block: String, - mentions: &[Mention], - language_registry: &Arc, - language: Option<&Arc>, -) -> RichText { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - render_markdown_mut( - &block, - mentions, - language_registry, - language, - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ); - text.truncate(text.trim_end().len()); - - RichText { - text: SharedString::from(text), - link_urls: link_urls.into(), - link_ranges, - highlights, - custom_ranges: Vec::new(), - custom_ranges_tooltip_fn: None, - } -} - pub fn render_code( text: &mut String, highlights: &mut Vec<(Range, Highlight)>, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7832af4b04..b3014d1748 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1880,22 +1880,70 @@ message CompleteWithLanguageModel { repeated LanguageModelRequestMessage messages = 2; repeated string stop = 3; float temperature = 4; + repeated ChatCompletionTool tools = 5; + optional string tool_choice = 6; } +// A tool presented to the language model for its use +message ChatCompletionTool { + oneof variant { + FunctionObject function = 1; + } + + message FunctionObject { + string name = 1; + optional string description = 2; + optional string parameters = 3; + } +} + +// A message to the language model message LanguageModelRequestMessage { LanguageModelRole role = 1; string content = 2; + optional string tool_call_id = 3; + repeated ToolCall tool_calls = 4; } enum LanguageModelRole { LanguageModelUser = 0; LanguageModelAssistant = 1; LanguageModelSystem = 2; + LanguageModelTool = 3; } message LanguageModelResponseMessage { optional LanguageModelRole role = 1; optional string content = 2; + repeated ToolCallDelta tool_calls = 3; +} + +// A request to call a tool, by the language model +message ToolCall { + string id = 1; + + oneof variant { + FunctionCall function = 2; + } + + message FunctionCall { + string name = 1; + string arguments = 2; + } +} + +message ToolCallDelta { + uint32 index = 1; + optional string id = 2; + + oneof variant { + FunctionCallDelta function = 3; + } + + message FunctionCallDelta { + optional string name = 1; + optional string arguments = 2; + } } message LanguageModelResponse { diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index f50f17934d..5f06d4193f 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -12,6 +12,11 @@ workspace = true [lib] path = "src/semantic_index.rs" +[[example]] +name = "index" +path = "examples/index.rs" +crate-type = ["bin"] + [dependencies] anyhow.workspace = true client.workspace = true diff --git a/crates/semantic_index/examples/index.rs b/crates/semantic_index/examples/index.rs index 494d8a0f81..6783e07048 100644 --- a/crates/semantic_index/examples/index.rs +++ b/crates/semantic_index/examples/index.rs @@ -1,25 +1,16 @@ use client::Client; use futures::channel::oneshot; -use gpui::{App, Global, TestAppContext}; +use gpui::{App, Global}; use language::language_settings::AllLanguageSettings; use project::Project; use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex}; use settings::SettingsStore; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use util::http::HttpClientWithUrl; -pub fn init_test(cx: &mut TestAppContext) { - _ = cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - language::init(cx); - Project::init_settings(cx); - SettingsStore::update(cx, |store, cx| { - store.update_user_settings::(cx, |_| {}); - }); - }); -} - fn main() { env_logger::init(); @@ -50,20 +41,21 @@ fn main() { // let embedding_provider = semantic_index::FakeEmbeddingProvider; let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set"); - let embedding_provider = OpenAiEmbeddingProvider::new( + + let embedding_provider = Arc::new(OpenAiEmbeddingProvider::new( http.clone(), OpenAiEmbeddingModel::TextEmbedding3Small, open_ai::OPEN_AI_API_URL.to_string(), api_key, - ); - - let semantic_index = SemanticIndex::new( - Path::new("/tmp/semantic-index-db.mdb"), - Arc::new(embedding_provider), - cx, - ); + )); cx.spawn(|mut cx| async move { + let semantic_index = SemanticIndex::new( + PathBuf::from("/tmp/semantic-index-db.mdb"), + embedding_provider, + &mut cx, + ); + let mut semantic_index = semantic_index.await.unwrap(); let project_path = Path::new(&args[1]); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index a43d9e177c..c3eccd95f6 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -21,7 +21,7 @@ use std::{ cmp::Ordering, future::Future, ops::Range, - path::Path, + path::{Path, PathBuf}, sync::Arc, time::{Duration, SystemTime}, }; @@ -37,30 +37,29 @@ pub struct SemanticIndex { impl Global for SemanticIndex {} impl SemanticIndex { - pub fn new( - db_path: &Path, + pub async fn new( + db_path: PathBuf, embedding_provider: Arc, - cx: &mut AppContext, - ) -> Task> { - let db_path = db_path.to_path_buf(); - cx.spawn(|cx| async move { - let db_connection = cx - .background_executor() - .spawn(async move { - unsafe { - heed::EnvOpenOptions::new() - .map_size(1024 * 1024 * 1024) - .max_dbs(3000) - .open(db_path) - } - }) - .await?; - - Ok(SemanticIndex { - db_connection, - embedding_provider, - project_indices: HashMap::default(), + cx: &mut AsyncAppContext, + ) -> Result { + let db_connection = cx + .background_executor() + .spawn(async move { + std::fs::create_dir_all(&db_path)?; + unsafe { + heed::EnvOpenOptions::new() + .map_size(1024 * 1024 * 1024) + .max_dbs(3000) + .open(db_path) + } }) + .await + .context("opening database connection")?; + + Ok(SemanticIndex { + db_connection, + embedding_provider, + project_indices: HashMap::default(), }) } @@ -91,7 +90,7 @@ pub struct ProjectIndex { worktree_indices: HashMap, language_registry: Arc, fs: Arc, - last_status: Status, + pub last_status: Status, embedding_provider: Arc, _subscription: Subscription, } @@ -397,7 +396,7 @@ impl WorktreeIndex { ) -> impl Future> { let worktree = self.worktree.read(cx).as_local().unwrap().snapshot(); let worktree_abs_path = worktree.abs_path().clone(); - let scan = self.scan_updated_entries(worktree, updated_entries, cx); + let scan = self.scan_updated_entries(worktree, updated_entries.clone(), cx); let chunk = self.chunk_files(worktree_abs_path, scan.updated_entries, cx); let embed = self.embed_files(chunk.files, cx); let persist = self.persist_embeddings(scan.deleted_entry_ranges, embed.files, cx); @@ -498,7 +497,9 @@ impl WorktreeIndex { | project::PathChange::Updated | project::PathChange::AddedOrUpdated => { if let Some(entry) = worktree.entry_for_id(*entry_id) { - updated_entries_tx.send(entry.clone()).await?; + if entry.is_file() { + updated_entries_tx.send(entry.clone()).await?; + } } } project::PathChange::Removed => { @@ -539,7 +540,14 @@ impl WorktreeIndex { cx.spawn(async { while let Ok(entry) = entries.recv().await { let entry_abs_path = worktree_abs_path.join(&entry.path); - let Some(text) = fs.load(&entry_abs_path).await.log_err() else { + let Some(text) = fs + .load(&entry_abs_path) + .await + .with_context(|| { + format!("failed to read path {entry_abs_path:?}") + }) + .log_err() + else { continue; }; let language = language_registry @@ -683,7 +691,7 @@ impl WorktreeIndex { .context("failed to create read transaction")?; let db_entries = db.iter(&txn).context("failed to iterate database")?; for db_entry in db_entries { - let (_, db_embedded_file) = db_entry?; + let (_key, db_embedded_file) = db_entry?; for chunk in db_embedded_file.chunks { chunks_tx .send((db_embedded_file.path.clone(), chunk)) @@ -700,6 +708,7 @@ impl WorktreeIndex { cx.spawn(|cx| async move { #[cfg(debug_assertions)] let embedding_query_start = std::time::Instant::now(); + log::info!("Searching for {query}"); let mut query_embeddings = embedding_provider .embed(&[TextToEmbed::new(&query)]) @@ -876,17 +885,13 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); - let mut semantic_index = cx - .update(|cx| { - let semantic_index = SemanticIndex::new( - Path::new(temp_dir.path()), - Arc::new(TestEmbeddingProvider), - cx, - ); - semantic_index - }) - .await - .unwrap(); + let mut semantic_index = SemanticIndex::new( + temp_dir.path().into(), + Arc::new(TestEmbeddingProvider), + &mut cx.to_async(), + ) + .await + .unwrap(); let project_path = Path::new("./fixture"); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index e646e37f2c..e716ef5b07 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,6 +2,7 @@ mod keymap_file; mod settings_file; mod settings_store; +use gpui::AppContext; use rust_embed::RustEmbed; use std::{borrow::Cow, str}; use util::asset_str; @@ -19,6 +20,14 @@ pub use settings_store::{ #[exclude = "*.DS_Store"] pub struct SettingsAssets; +pub fn init(cx: &mut AppContext) { + let mut settings = SettingsStore::default(); + settings + .set_default_settings(&default_settings(), cx) + .unwrap(); + cx.set_global(settings); +} + pub fn default_settings() -> Cow<'static, str> { asset_str::("settings/default.json") } diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index c238542478..dcc42546fe 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -29,14 +29,14 @@ pub enum ComponentStory { ListHeader, ListItem, OverflowScroll, + Picker, Scroll, Tab, TabBar, + Text, TitleBar, ToggleButton, - Text, ViewportUnits, - Picker, } impl ComponentStory { diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 015b4765fb..70853267ca 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -11,7 +11,7 @@ use gpui::{ }; use log::LevelFilter; use project::Project; -use settings::{default_settings, KeymapFile, Settings, SettingsStore}; +use settings::{KeymapFile, Settings}; use simplelog::SimpleLogger; use strum::IntoEnumIterator; use theme::{ThemeRegistry, ThemeSettings}; @@ -64,12 +64,7 @@ fn main() { gpui::App::new().with_assets(Assets).run(move |cx| { load_embedded_fonts(cx).unwrap(); - let mut store = SettingsStore::default(); - store - .set_default_settings(default_settings().as_ref(), cx) - .unwrap(); - cx.set_global(store); - + settings::init(cx); theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); let selector = story_selector; @@ -122,7 +117,7 @@ impl Render for StoryWrapper { .flex() .flex_col() .size_full() - .font("Zed Mono") + .font_family("Zed Mono") .child(self.story.clone()) } } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 2a38130720..b93a997fe7 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,6 +1,7 @@ mod avatar; mod button; mod checkbox; +mod collapsible_container; mod context_menu; mod disclosure; mod divider; @@ -25,6 +26,7 @@ mod stories; pub use avatar::*; pub use button::*; pub use checkbox::*; +pub use collapsible_container::*; pub use context_menu::*; pub use disclosure::*; pub use divider::*; diff --git a/crates/ui/src/components/collapsible_container.rs b/crates/ui/src/components/collapsible_container.rs new file mode 100644 index 0000000000..5136dbd13d --- /dev/null +++ b/crates/ui/src/components/collapsible_container.rs @@ -0,0 +1,152 @@ +use crate::{prelude::*, ButtonLike}; +use smallvec::SmallVec; + +use gpui::*; + +#[derive(Default, Clone, Copy, Debug, PartialEq)] +pub enum ContainerStyle { + #[default] + None, + Card, +} + +struct ContainerStyles { + pub background_color: Hsla, + pub border_color: Hsla, + pub text_color: Hsla, +} + +#[derive(IntoElement)] +pub struct CollapsibleContainer { + id: ElementId, + base: ButtonLike, + toggle: bool, + /// A slot for content that appears before the label, like an icon or avatar. + start_slot: Option, + /// A slot for content that appears after the label, usually on the other side of the header. + /// This might be a button, a disclosure arrow, a face pile, etc. + end_slot: Option, + style: ContainerStyle, + children: SmallVec<[AnyElement; 1]>, +} + +impl CollapsibleContainer { + pub fn new(id: impl Into, toggle: bool) -> Self { + Self { + id: id.into(), + base: ButtonLike::new("button_base"), + toggle, + start_slot: None, + end_slot: None, + style: ContainerStyle::Card, + children: SmallVec::new(), + } + } + + pub fn start_slot(mut self, start_slot: impl Into>) -> Self { + self.start_slot = start_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn end_slot(mut self, end_slot: impl Into>) -> Self { + self.end_slot = end_slot.into().map(IntoElement::into_any_element); + self + } + + pub fn child(mut self, child: E) -> Self { + self.children.push(child.into_any_element()); + self + } +} + +impl Clickable for CollapsibleContainer { + fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { + self.base = self.base.on_click(handler); + self + } +} + +impl RenderOnce for CollapsibleContainer { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let color = cx.theme().colors(); + + let styles = match self.style { + ContainerStyle::None => ContainerStyles { + background_color: color.ghost_element_background, + border_color: color.border_transparent, + text_color: color.text, + }, + ContainerStyle::Card => ContainerStyles { + background_color: color.elevated_surface_background, + border_color: color.border, + text_color: color.text, + }, + }; + + v_flex() + .id(self.id) + .relative() + .rounded_md() + .bg(styles.background_color) + .border() + .border_color(styles.border_color) + .text_color(styles.text_color) + .overflow_hidden() + .child( + h_flex() + .overflow_hidden() + .w_full() + .group("toggleable_container_header") + .border_b() + .border_color(if self.toggle { + styles.border_color + } else { + color.border_transparent + }) + .child( + self.base.full_width().style(ButtonStyle::Subtle).child( + div() + .h_7() + .p_1() + .flex() + .flex_1() + .items_center() + .justify_between() + .w_full() + .gap_1() + .cursor_pointer() + .group_hover("toggleable_container_header", |this| { + this.bg(color.element_hover) + }) + .child( + h_flex() + .gap_1() + .child( + IconButton::new( + "toggle_icon", + match self.toggle { + true => IconName::ChevronDown, + false => IconName::ChevronRight, + }, + ) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall), + ) + .child( + div() + .id("label_container") + .flex() + .gap_1() + .items_center() + .children(self.start_slot), + ), + ) + .child(h_flex().children(self.end_slot)), + ), + ), + ) + .when(self.toggle, |this| { + this.child(h_flex().flex_1().w_full().p_1().children(self.children)) + }) + } +} diff --git a/crates/ui/src/components/title_bar/windows_window_controls.rs b/crates/ui/src/components/title_bar/windows_window_controls.rs index 8352bed678..7c12395168 100644 --- a/crates/ui/src/components/title_bar/windows_window_controls.rs +++ b/crates/ui/src/components/title_bar/windows_window_controls.rs @@ -110,7 +110,7 @@ impl RenderOnce for WindowsCaptionButton { .content_center() .w(width) .h_full() - .font("Segoe Fluent Icons") + .font_family("Segoe Fluent Icons") .text_size(px(10.0)) .hover(|style| style.bg(self.hover_background_color)) .active(|style| { diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 1ce25129ff..5d07f6b341 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -95,7 +95,7 @@ pub fn tooltip_container( div().pl_2().pt_2p5().child( v_flex() .elevation_2(cx) - .font(ui_font) + .font_family(ui_font) .text_ui() .text_color(cx.theme().colors().text) .py_1() diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index b4b598a7c2..cd40cb1e99 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -93,7 +93,7 @@ impl RenderOnce for Headline { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); div() - .font(ui_font) + .font_family(ui_font) .line_height(self.size.line_height()) .text_size(self.size.size()) .text_color(cx.theme().colors().text) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c4f62715b3..ddc81a3e12 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2928,6 +2928,6 @@ impl Render for DraggedTab { .selected(self.is_active) .child(label) .render(cx) - .font(ui_font) + .font_family(ui_font) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2a6ae60701..94890bc15c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4004,7 +4004,7 @@ impl Render for Workspace { .size_full() .flex() .flex_col() - .font(ui_font) + .font_family(ui_font) .gap_0() .justify_start() .items_start() diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d005138dba..2eb188f768 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 audio.workspace = true auto_update.workspace = true backtrace = "0.3" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3bc06f9ac6..ea5aafcb66 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -231,27 +231,18 @@ fn init_ui(args: Args) { load_embedded_fonts(cx); - let mut store = SettingsStore::default(); - store - .set_default_settings(default_settings().as_ref(), cx) - .unwrap(); - cx.set_global(store); + settings::init(cx); handle_settings_file_changes(user_settings_file_rx, cx); handle_keymap_file_changes(user_keymap_file_rx, cx); + client::init_settings(cx); - - let clock = Arc::new(clock::RealSystemClock); - let http = Arc::new(HttpClientWithUrl::new( - &client::ClientSettings::get_global(cx).server_url, - )); - - let client = client::Client::new(clock, http.clone(), cx); + let client = Client::production(cx); let mut languages = LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone()); let copilot_language_server_id = languages.next_language_server_id(); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let node_runtime = RealNodeRuntime::new(http.clone()); + let node_runtime = RealNodeRuntime::new(client.http_client()); language::init(cx); languages::init(languages.clone(), node_runtime.clone(), cx); @@ -271,11 +262,14 @@ fn init_ui(args: Args) { diagnostics::init(cx); copilot::init( copilot_language_server_id, - http.clone(), + client.http_client(), node_runtime.clone(), cx, ); + assistant::init(client.clone(), cx); + assistant2::init(client.clone(), cx); + init_inline_completion_provider(client.telemetry().clone(), cx); extension::init( @@ -297,7 +291,7 @@ fn init_ui(args: Args) { cx.observe_global::({ let languages = languages.clone(); - let http = http.clone(); + let http = client.http_client(); let client = client.clone(); move |cx| { @@ -345,7 +339,7 @@ fn init_ui(args: Args) { AppState::set_global(Arc::downgrade(&app_state), cx); audio::init(Assets, cx); - auto_update::init(http.clone(), cx); + auto_update::init(client.http_client(), cx); workspace::init(app_state.clone(), cx); recent_projects::init(cx); @@ -378,7 +372,7 @@ fn init_ui(args: Args) { initialize_workspace(app_state.clone(), cx); // todo(linux): unblock this - upload_panics_and_crashes(http.clone(), cx); + upload_panics_and_crashes(client.http_client(), cx); cx.activate(true); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 963b7c3237..fbbec18601 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3,7 +3,6 @@ mod only_instance; mod open_listener; pub use app_menus::*; -use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; use client::ZED_URL_SCHEME; use collections::VecDeque; @@ -181,10 +180,12 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) }); } + cx.spawn(|workspace_handle, mut cx| async move { + let assistant_panel = + assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone()); let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let channels_panel = collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); let chat_panel = @@ -193,6 +194,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { workspace_handle.clone(), cx.clone(), ); + let ( project_panel, terminal_panel, @@ -210,9 +212,9 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { )?; workspace_handle.update(&mut cx, |workspace, cx| { + workspace.add_panel(assistant_panel, cx); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); - workspace.add_panel(assistant_panel, cx); workspace.add_panel(channels_panel, cx); workspace.add_panel(chat_panel, cx); workspace.add_panel(notification_panel, cx); @@ -221,6 +223,30 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .detach(); + let mut current_user = app_state.user_store.read(cx).watch_current_user(); + + cx.spawn(|workspace_handle, mut cx| async move { + while let Some(user) = current_user.next().await { + if user.is_some() { + // User known now, can check feature flags / staff + // At this point, should have the user with staff status available + let use_assistant2 = cx.update(|cx| assistant2::enabled(cx))?; + if use_assistant2 { + let panel = + assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()) + .await?; + workspace_handle.update(&mut cx, |workspace, cx| { + workspace.add_panel(panel, cx); + })?; + } + + break; + } + } + anyhow::Ok(()) + }) + .detach(); + workspace .register_action(about) .register_action(|_, _: &Minimize, cx| { @@ -3028,11 +3054,7 @@ mod tests { ]) .unwrap(); let themes = ThemeRegistry::default(); - let mut settings = SettingsStore::default(); - settings - .set_default_settings(&settings::default_settings(), cx) - .unwrap(); - cx.set_global(settings); + settings::init(cx); theme::init(theme::LoadThemes::JustBase, cx); let mut has_default_theme = false; diff --git a/script/zed-local b/script/zed-local index 0ab6f0d0d1..69a44fe94a 100755 --- a/script/zed-local +++ b/script/zed-local @@ -147,7 +147,7 @@ setTimeout(() => { } spawn(binaryPath, i == 0 ? args : [], { stdio: "inherit", - env: { + env: Object.assign({}, process.env, { ZED_IMPERSONATE: users[i], ZED_WINDOW_POSITION: position, ZED_STATELESS: isStateful && i == 0 ? "1" : "", @@ -157,9 +157,8 @@ setTimeout(() => { ZED_ADMIN_API_TOKEN: "secret", ZED_WINDOW_SIZE: size, ZED_CLIENT_CHECKSUM_SEED: "development-checksum-seed", - PATH: process.env.PATH, RUST_LOG: process.env.RUST_LOG || "info", - }, + }), }); } }, 0.1); From cf67fc90551d79baad178a16841f6fb13f8892dd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 23 Apr 2024 19:38:57 -0400 Subject: [PATCH 029/101] Add `language_servers` setting for customizing which language servers run (#10911) This PR adds a new `language_servers` setting underneath the language settings. This setting controls which of the available language servers for a given language will run. The `language_servers` setting is an array of strings. Each item in the array must be either: - A language server ID (e.g., `"rust-analyzer"`, `"typescript-language-server"`, `"eslint"`, etc.) denoting a language server that should be enabled. - A language server ID prefixed with a `!` (e.g., `"!rust-analyzer"`, `"!typescript-language-server"`, `"!eslint"`, etc.) denoting a language server that should be disabled. - A `"..."` placeholder, which will be replaced by the remaining available language servers that haven't already been mentioned in the array. For example, to enable the Biome language server in place of the default TypeScript language server, you would add the following to your settings: ```json { "languages": { "TypeScript": { "language_servers": ["biome", "!typescript-language-server", "..."] } } } ``` More details can be found in #10906. Release Notes: - Added `language_servers` setting to language settings for customizing which language server(s) run for a given language. --- Cargo.lock | 1 + assets/settings/default.json | 4 + crates/language/Cargo.toml | 5 +- crates/language/src/language_settings.rs | 147 +++++++++++++++++++++-- crates/project/src/project.rs | 15 ++- 5 files changed, 162 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca625dd461..21761f96e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5454,6 +5454,7 @@ dependencies = [ "globset", "gpui", "indoc", + "itertools 0.11.0", "lazy_static", "log", "lsp", diff --git a/assets/settings/default.json b/assets/settings/default.json index c5ee98abf0..2ba2268b43 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -294,6 +294,10 @@ "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. "enable_language_server": true, + // The list of language servers to use (or disable) for all languages. + // + // This is typically customized on a per-language basis. + "language_servers": ["..."], // When to automatically save edited buffers. This setting can // take four values. // diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b513fbb255..85819362b1 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -34,11 +34,13 @@ fuzzy.workspace = true git.workspace = true globset.workspace = true gpui.workspace = true +itertools.workspace = true lazy_static.workspace = true log.workspace = true lsp.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark.workspace = true rand = { workspace = true, optional = true } regex.workspace = true rpc.workspace = true @@ -50,15 +52,14 @@ similar = "1.3" smallvec.workspace = true smol.workspace = true sum_tree.workspace = true +task.workspace = true text.workspace = true theme.workspace = true tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } -pulldown-cmark.workspace = true tree-sitter.workspace = true unicase = "2.6" util.workspace = true -task.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 3672fb1e15..e65b823bc1 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,10 +1,11 @@ //! Provides `language`-related settings. -use crate::{File, Language}; +use crate::{File, Language, LanguageServerName}; use anyhow::Result; use collections::{HashMap, HashSet}; use globset::GlobMatcher; use gpui::AppContext; +use itertools::{Either, Itertools}; use schemars::{ schema::{InstanceType, ObjectValidation, Schema, SchemaObject}, JsonSchema, @@ -92,6 +93,13 @@ pub struct LanguageSettings { pub prettier: HashMap, /// Whether to use language servers to provide code intelligence. pub enable_language_server: bool, + /// The list of language servers to use (or disable) for this language. + /// + /// This array should consist of language server IDs, as well as the following + /// special tokens: + /// - `"!"` - A language server ID prefixed with a `!` will be disabled. + /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. + pub language_servers: Vec>, /// Controls whether Copilot provides suggestion immediately (true) /// or waits for a `copilot::Toggle` (false). pub show_copilot_suggestions: bool, @@ -109,6 +117,53 @@ pub struct LanguageSettings { pub code_actions_on_format: HashMap, } +impl LanguageSettings { + /// A token representing the rest of the available language servers. + const REST_OF_LANGUAGE_SERVERS: &'static str = "..."; + + /// Returns the customized list of language servers from the list of + /// available language servers. + pub fn customized_language_servers( + &self, + available_language_servers: &[LanguageServerName], + ) -> Vec { + Self::resolve_language_servers(&self.language_servers, available_language_servers) + } + + pub(crate) fn resolve_language_servers( + configured_language_servers: &[Arc], + available_language_servers: &[LanguageServerName], + ) -> Vec { + let (disabled_language_servers, enabled_language_servers): (Vec>, Vec>) = + configured_language_servers.iter().partition_map( + |language_server| match language_server.strip_prefix('!') { + Some(disabled) => Either::Left(disabled.into()), + None => Either::Right(language_server.clone()), + }, + ); + + let rest = available_language_servers + .into_iter() + .filter(|&available_language_server| { + !disabled_language_servers.contains(&&available_language_server.0) + && !enabled_language_servers.contains(&&available_language_server.0) + }) + .cloned() + .collect::>(); + + enabled_language_servers + .into_iter() + .flat_map(|language_server| { + if language_server.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { + rest.clone() + } else { + vec![LanguageServerName(language_server.clone())] + } + }) + .collect::>() + } +} + /// The settings for [GitHub Copilot](https://github.com/features/copilot). #[derive(Clone, Debug, Default)] pub struct CopilotSettings { @@ -119,7 +174,7 @@ pub struct CopilotSettings { } /// The settings for all languages. -#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. #[serde(default)] @@ -140,7 +195,7 @@ pub struct AllLanguageSettingsContent { } /// The settings for a particular language. -#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. /// @@ -211,6 +266,16 @@ pub struct LanguageSettingsContent { /// Default: true #[serde(default)] pub enable_language_server: Option, + /// The list of language servers to use (or disable) for this language. + /// + /// This array should consist of language server IDs, as well as the following + /// special tokens: + /// - `"!"` - A language server ID prefixed with a `!` will be disabled. + /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. + /// + /// Default: ["..."] + #[serde(default)] + pub language_servers: Option>>, /// Controls whether Copilot provides suggestion immediately (true) /// or waits for a `copilot::Toggle` (false). /// @@ -257,7 +322,7 @@ pub struct CopilotSettingsContent { } /// The settings for enabling/disabling features. -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { /// Whether the GitHub Copilot feature is enabled. @@ -608,6 +673,12 @@ impl settings::Settings for AllLanguageSettings { } fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } + merge(&mut settings.tab_size, src.tab_size); merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); @@ -642,6 +713,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent &mut settings.enable_language_server, src.enable_language_server, ); + merge(&mut settings.language_servers, src.language_servers.clone()); merge( &mut settings.show_copilot_suggestions, src.show_copilot_suggestions, @@ -652,9 +724,70 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.extend_comment_on_newline, ); merge(&mut settings.inlay_hints, src.inlay_hints); - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_resolve_language_servers() { + fn language_server_names(names: &[&str]) -> Vec { + names + .into_iter() + .copied() + .map(|name| LanguageServerName(name.into())) + .collect::>() } + + let available_language_servers = language_server_names(&[ + "typescript-language-server", + "biome", + "deno", + "eslint", + "tailwind", + ]); + + // A value of just `["..."]` is the same as taking all of the available language servers. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()], + &available_language_servers, + ), + available_language_servers + ); + + // Referencing one of the available language servers will change its order. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[ + "biome".into(), + LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(), + "deno".into() + ], + &available_language_servers + ), + language_server_names(&[ + "biome", + "typescript-language-server", + "eslint", + "tailwind", + "deno", + ]) + ); + + // Negating an available language server removes it from the list. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[ + "deno".into(), + "!typescript-language-server".into(), + "!biome".into(), + LanguageSettings::REST_OF_LANGUAGE_SERVERS.into() + ], + &available_language_servers + ), + language_server_names(&["deno", "eslint", "tailwind"]) + ); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad7b67ffe0..019fca5ca2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3060,7 +3060,20 @@ impl Project { return; } - for adapter in self.languages.clone().lsp_adapters(&language) { + let available_lsp_adapters = self.languages.clone().lsp_adapters(&language); + let available_language_servers = available_lsp_adapters + .iter() + .map(|lsp_adapter| lsp_adapter.name.clone()) + .collect::>(); + + let enabled_language_servers = + settings.customized_language_servers(&available_language_servers); + + for adapter in available_lsp_adapters { + if !enabled_language_servers.contains(&adapter.name) { + continue; + } + self.start_language_server(worktree, adapter.clone(), language.clone(), cx); } } From 25981550d5605e4d17a0ecc6f39920c3b0195d04 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 23 Apr 2024 20:44:11 -0400 Subject: [PATCH 030/101] Extract Deno extension (#10912) This PR extracts Deno support into an extension and removes the built-in Deno support from Zed. When using the Deno extension, you'll want to add the following to your settings to disable the built-in TypeScript and ESLint language servers so that they don't conflict with Deno's functionality: ```json { "languages": { "TypeScript": { "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."] }, "TSX": { "language_servers": ["deno", "!typescript-language-server", "!eslint", "..."] } } } ``` Release Notes: - Removed built-in support for Deno, in favor of making it available as an extension. --- Cargo.lock | 7 + Cargo.toml | 1 + assets/settings/default.json | 4 - crates/languages/src/deno.rs | 228 --------------------------------- crates/languages/src/lib.rs | 119 +++++++---------- extensions/deno/Cargo.toml | 16 +++ extensions/deno/LICENSE-APACHE | 1 + extensions/deno/extension.toml | 13 ++ extensions/deno/src/deno.rs | 154 ++++++++++++++++++++++ 9 files changed, 236 insertions(+), 307 deletions(-) delete mode 100644 crates/languages/src/deno.rs create mode 100644 extensions/deno/Cargo.toml create mode 120000 extensions/deno/LICENSE-APACHE create mode 100644 extensions/deno/extension.toml create mode 100644 extensions/deno/src/deno.rs diff --git a/Cargo.lock b/Cargo.lock index 21761f96e2..85bc6dfd0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12728,6 +12728,13 @@ dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zed_deno" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_elm" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index adb9f461a6..7a883e4586 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ members = [ "extensions/clojure", "extensions/csharp", "extensions/dart", + "extensions/deno", "extensions/elm", "extensions/emmet", "extensions/erlang", diff --git a/assets/settings/default.json b/assets/settings/default.json index 2ba2268b43..5e90ba524c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -576,10 +576,6 @@ // "lsp": "elixir_ls" }, - // Settings specific to our deno integration - "deno": { - "enable": false - }, "code_actions_on_format": {}, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should diff --git a/crates/languages/src/deno.rs b/crates/languages/src/deno.rs deleted file mode 100644 index b43411c674..0000000000 --- a/crates/languages/src/deno.rs +++ /dev/null @@ -1,228 +0,0 @@ -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use collections::HashMap; -use futures::StreamExt; -use gpui::AppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; -use lsp::{CodeActionKind, LanguageServerBinary}; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use serde_json::json; -use settings::{Settings, SettingsSources}; -use smol::{fs, fs::File}; -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; -use util::{ - fs::remove_matching, - github::{latest_github_release, GitHubLspBinaryVersion}, - maybe, ResultExt, -}; - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct DenoSettings { - pub enable: bool, -} - -#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] -pub struct DenoSettingsContent { - enable: Option, -} - -impl Settings for DenoSettings { - const KEY: Option<&'static str> = Some("deno"); - - type FileContent = DenoSettingsContent; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - sources.json_merge() - } -} - -fn deno_server_binary_arguments() -> Vec { - vec!["lsp".into()] -} - -pub struct DenoLspAdapter {} - -impl DenoLspAdapter { - pub fn new() -> Self { - DenoLspAdapter {} - } -} - -#[async_trait(?Send)] -impl LspAdapter for DenoLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("deno-language-server".into()) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let release = - latest_github_release("denoland/deno", true, false, delegate.http_client()).await?; - let os = match consts::OS { - "macos" => "apple-darwin", - "linux" => "unknown-linux-gnu", - "windows" => "pc-windows-msvc", - other => bail!("Running on unsupported os: {other}"), - }; - let asset_name = format!("deno-{}-{os}.zip", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; - let version = GitHubLspBinaryVersion { - name: release.tag_name, - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - let zip_path = container_dir.join(format!("deno_{}.zip", version.name)); - let version_dir = container_dir.join(format!("deno_{}", version.name)); - let binary_path = version_dir.join("deno"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .context("error downloading release")?; - let mut file = File::create(&zip_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response.body_mut(), &mut file).await?; - - let unzip_status = smol::process::Command::new("unzip") - .current_dir(&container_dir) - .arg(&zip_path) - .arg("-d") - .arg(&version_dir) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip deno archive"))?; - } - - remove_matching(&container_dir, |entry| entry != version_dir).await; - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: deno_server_binary_arguments(), - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary(container_dir).await - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary(container_dir).await - } - - fn code_action_kinds(&self) -> Option> { - Some(vec![ - CodeActionKind::QUICKFIX, - CodeActionKind::REFACTOR, - CodeActionKind::REFACTOR_EXTRACT, - CodeActionKind::SOURCE, - ]) - } - - async fn label_for_completion( - &self, - item: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - use lsp::CompletionItemKind as Kind; - let len = item.label.len(); - let grammar = language.grammar()?; - let highlight_id = match item.kind? { - Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), - Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), - Kind::CONSTANT => grammar.highlight_id_for_name("constant"), - Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), - Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), - _ => None, - }?; - - let text = match &item.detail { - Some(detail) => format!("{} {}", item.label, detail), - None => item.label.clone(), - }; - - Some(language::CodeLabel { - text, - runs: vec![(0..len, highlight_id)], - filter_range: 0..len, - }) - } - - async fn initialization_options( - self: Arc, - _: &Arc, - ) -> Result> { - Ok(Some(json!({ - "provideFormatter": true, - }))) - } - - fn language_ids(&self) -> HashMap { - HashMap::from_iter([ - ("TypeScript".into(), "typescript".into()), - ("JavaScript".into(), "javascript".into()), - ("TSX".into(), "typescriptreact".into()), - ]) - } -} - -async fn get_cached_server_binary(container_dir: PathBuf) -> Option { - maybe!(async { - let mut last = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - last = Some(entry?.path()); - } - - match last { - Some(path) if path.is_dir() => { - let binary = path.join("deno"); - if fs::metadata(&binary).await.is_ok() { - return Ok(LanguageServerBinary { - path: binary, - env: None, - arguments: deno_server_binary_arguments(), - }); - } - } - _ => {} - } - - Err(anyhow!("no cached binary")) - }) - .await - .log_err() -} diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 44d9c02b4a..a25368f903 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -13,12 +13,11 @@ use crate::{ rust::RustContextProvider, }; -use self::{deno::DenoSettings, elixir::ElixirSettings}; +use self::elixir::ElixirSettings; mod bash; mod c; mod css; -mod deno; mod elixir; mod go; mod json; @@ -49,7 +48,6 @@ pub fn init( cx: &mut AppContext, ) { ElixirSettings::register(cx); - DenoSettings::register(cx); languages.register_native_grammars([ ("bash", tree_sitter_bash::language()), @@ -193,58 +191,33 @@ pub fn init( vec![Arc::new(rust::RustLspAdapter)], RustContextProvider ); - match &DenoSettings::get(None, cx).enable { - true => { - language!( - "tsx", - vec![ - Arc::new(deno::DenoLspAdapter::new()), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); - language!("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]); - language!( - "javascript", - vec![ - Arc::new(deno::DenoLspAdapter::new()), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); - language!("jsdoc", vec![Arc::new(deno::DenoLspAdapter::new())]); - } - false => { - language!( - "tsx", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); - language!( - "typescript", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ] - ); - language!( - "javascript", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); - language!( - "jsdoc", - vec![Arc::new(typescript::TypeScriptLspAdapter::new( - node_runtime.clone(), - ))] - ); - } - } - + language!( + "tsx", + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ] + ); + language!( + "typescript", + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ] + ); + language!( + "javascript", + vec![ + Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ] + ); + language!( + "jsdoc", + vec![Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone(), + ))] + ); language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); language!( "erb", @@ -260,26 +233,22 @@ pub fn init( ); language!("proto"); - languages.register_secondary_lsp_adapter( - "Astro".into(), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ); - languages.register_secondary_lsp_adapter( - "HTML".into(), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ); - languages.register_secondary_lsp_adapter( - "PHP".into(), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ); - languages.register_secondary_lsp_adapter( - "Svelte".into(), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ); - languages.register_secondary_lsp_adapter( - "Vue.js".into(), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ); + let tailwind_languages = [ + "Astro", + "HTML", + "PHP", + "Svelte", + "TSX", + "JavaScript", + "Vue.js", + ]; + + for language in tailwind_languages { + languages.register_secondary_lsp_adapter( + language.into(), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ); + } let mut subscription = languages.subscribe(); let mut prev_language_settings = languages.language_settings(); diff --git a/extensions/deno/Cargo.toml b/extensions/deno/Cargo.toml new file mode 100644 index 0000000000..59253c6cdc --- /dev/null +++ b/extensions/deno/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_deno" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/deno.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/deno/LICENSE-APACHE b/extensions/deno/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/deno/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/deno/extension.toml b/extensions/deno/extension.toml new file mode 100644 index 0000000000..8efcb63e06 --- /dev/null +++ b/extensions/deno/extension.toml @@ -0,0 +1,13 @@ +id = "deno" +name = "Deno" +description = "Deno support." +version = "0.0.1" +schema_version = 1 +authors = ["Lino Le Van <11367844+lino-levan@users.noreply.github.com>"] +repository = "https://github.com/zed-industries/zed" + +[language_servers.deno] +name = "Deno Language Server" +languages = ["TypeScript", "TSX", "JavaScript", "JSDoc"] +language_ids = { "TypeScript" = "typescript", "TSX" = "typescriptreact", "JavaScript" = "javascript" } +code_action_kinds = ["quickfix", "refactor", "refactor.extract", "source"] diff --git a/extensions/deno/src/deno.rs b/extensions/deno/src/deno.rs new file mode 100644 index 0000000000..02231765d5 --- /dev/null +++ b/extensions/deno/src/deno.rs @@ -0,0 +1,154 @@ +use std::fs; +use zed::lsp::CompletionKind; +use zed::{serde_json, CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +struct DenoExtension { + cached_binary_path: Option, +} + +impl DenoExtension { + fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = worktree.which("deno") { + return Ok(path); + } + + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "denoland/deno", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "deno-{arch}-{os}.zip", + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X8664 => "x86_64", + zed::Architecture::X86 => + return Err(format!("unsupported architecture: {arch:?}")), + }, + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-gnu", + zed::Os::Windows => "pc-windows-msvc", + }, + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("deno-{}", release.version); + let binary_path = format!("{version_dir}/deno"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::Zip, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } +} + +impl zed::Extension for DenoExtension { + fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + Ok(zed::Command { + command: self.language_server_binary_path(language_server_id, worktree)?, + args: vec!["lsp".to_string()], + env: Default::default(), + }) + } + + fn language_server_initialization_options( + &mut self, + _language_server_id: &zed::LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result> { + Ok(Some(serde_json::json!({ + "provideFormatter": true, + }))) + } + + fn label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let highlight_name = match completion.kind? { + CompletionKind::Class | CompletionKind::Interface | CompletionKind::Constructor => { + "type" + } + CompletionKind::Constant => "constant", + CompletionKind::Function | CompletionKind::Method => "function", + CompletionKind::Property | CompletionKind::Field => "property", + _ => return None, + }; + + let len = completion.label.len(); + let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string())); + + Some(zed::CodeLabel { + code: Default::default(), + spans: if let Some(detail) = completion.detail { + vec![ + name_span, + CodeLabelSpan::literal(" ", None), + CodeLabelSpan::literal(detail, None), + ] + } else { + vec![name_span] + }, + filter_range: (0..len).into(), + }) + } +} + +zed::register_extension!(DenoExtension); From af5a9fabc61285883e8a6a808afc149587cfb0a2 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Tue, 23 Apr 2024 20:49:29 -0700 Subject: [PATCH 031/101] Include root schema as parameters for tool calling (#10914) Allows `LanguageModelTool`s to include nested structures, by exposing the definitions section of their JSON Schema. Release Notes: - N/A --- Cargo.lock | 1 + crates/assistant2/Cargo.toml | 1 + .../examples/chat-with-functions.rs | 218 ++++++++++++++++++ crates/assistant_tooling/src/registry.rs | 3 +- crates/assistant_tooling/src/tool.rs | 23 +- 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 crates/assistant2/examples/chat-with-functions.rs diff --git a/Cargo.lock b/Cargo.lock index 85bc6dfd0b..0caa43fc13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,6 +391,7 @@ dependencies = [ "node_runtime", "open_ai", "project", + "rand 0.8.5", "release_channel", "rich_text", "schemars", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 060dbaa98b..886a84c863 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -46,6 +46,7 @@ language = { workspace = true, features = ["test-support"] } languages.workspace = true node_runtime.workspace = true project = { workspace = true, features = ["test-support"] } +rand.workspace = true release_channel.workspace = true settings = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant2/examples/chat-with-functions.rs b/crates/assistant2/examples/chat-with-functions.rs new file mode 100644 index 0000000000..15d3c968a4 --- /dev/null +++ b/crates/assistant2/examples/chat-with-functions.rs @@ -0,0 +1,218 @@ +use anyhow::Context as _; +use assets::Assets; +use assistant2::AssistantPanel; +use assistant_tooling::{LanguageModelTool, ToolRegistry}; +use client::Client; +use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Task, View, WindowOptions}; +use language::LanguageRegistry; +use project::Project; +use rand::Rng; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{KeymapFile, DEFAULT_KEYMAP_PATH}; +use std::sync::Arc; +use theme::LoadThemes; +use ui::{div, prelude::*, Render}; +use util::ResultExt as _; + +actions!(example, [Quit]); + +struct RollDiceTool {} + +impl RollDiceTool { + fn new() -> Self { + Self {} + } +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +enum Die { + D6 = 6, + D20 = 20, +} + +impl Die { + fn into_str(&self) -> &'static str { + match self { + Die::D6 => "d6", + Die::D20 => "d20", + } + } +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone)] +struct DiceParams { + /// The number of dice to roll. + num_dice: u8, + /// Which die to roll. Defaults to a d6 if not provided. + die_type: Option, +} + +#[derive(Serialize, Deserialize)] +struct DieRoll { + die: Die, + roll: u8, +} + +impl DieRoll { + fn render(&self) -> AnyElement { + match self.die { + Die::D6 => { + let face = match self.roll { + 6 => div().child("⚅"), + 5 => div().child("⚄"), + 4 => div().child("⚃"), + 3 => div().child("⚂"), + 2 => div().child("⚁"), + 1 => div().child("⚀"), + _ => div().child("😅"), + }; + face.text_3xl().into_any_element() + } + _ => div() + .child(format!("{}", self.roll)) + .text_3xl() + .into_any_element(), + } + } +} + +#[derive(Serialize, Deserialize)] +struct DiceRoll { + rolls: Vec, +} + +impl LanguageModelTool for RollDiceTool { + type Input = DiceParams; + type Output = DiceRoll; + + fn name(&self) -> String { + "roll_dice".to_string() + } + + fn description(&self) -> String { + "Rolls N many dice and returns the results.".to_string() + } + + fn execute(&self, input: &Self::Input, _cx: &AppContext) -> Task> { + let rolls = (0..input.num_dice) + .map(|_| { + let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone(); + + DieRoll { + die: die_type.clone(), + roll: rand::thread_rng().gen_range(1..=die_type as u8), + } + }) + .collect(); + + return Task::ready(Ok(DiceRoll { rolls })); + } + + fn render( + _tool_call_id: &str, + _input: &Self::Input, + output: &Self::Output, + _cx: &mut WindowContext, + ) -> gpui::AnyElement { + h_flex() + .children( + output + .rolls + .iter() + .map(|roll| div().p_2().child(roll.render())), + ) + .into_any_element() + } + + fn format(_input: &Self::Input, output: &Self::Output) -> String { + let mut result = String::new(); + for roll in &output.rolls { + let die = &roll.die; + result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll)); + } + result + } +} + +fn main() { + env_logger::init(); + App::new().with_assets(Assets).run(|cx| { + cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None))); + cx.on_action(|_: &Quit, cx: &mut AppContext| { + cx.quit(); + }); + + settings::init(cx); + language::init(cx); + Project::init_settings(cx); + editor::init(cx); + theme::init(LoadThemes::JustBase, cx); + Assets.load_fonts(cx).unwrap(); + KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap(); + client::init_settings(cx); + release_channel::init("0.130.0", cx); + + let client = Client::production(cx); + { + let client = client.clone(); + cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await }) + .detach_and_log_err(cx); + } + assistant2::init(client.clone(), cx); + + let language_registry = Arc::new(LanguageRegistry::new( + Task::ready(()), + cx.background_executor().clone(), + )); + let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client()); + languages::init(language_registry.clone(), node_runtime, cx); + + cx.spawn(|cx| async move { + cx.update(|cx| { + let mut tool_registry = ToolRegistry::new(); + tool_registry + .register(RollDiceTool::new()) + .context("failed to register DummyTool") + .log_err(); + + let tool_registry = Arc::new(tool_registry); + + println!("Tools registered"); + for definition in tool_registry.definitions() { + println!("{}", definition); + } + + cx.open_window(WindowOptions::default(), |cx| { + cx.new_view(|cx| Example::new(language_registry, tool_registry, cx)) + }); + cx.activate(true); + }) + }) + .detach_and_log_err(cx); + }) +} + +struct Example { + assistant_panel: View, +} + +impl Example { + fn new( + language_registry: Arc, + tool_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + Self { + assistant_panel: cx + .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)), + } + } +} + +impl Render for Example { + fn render(&mut self, _cx: &mut ViewContext) -> impl ui::prelude::IntoElement { + div().size_full().child(self.assistant_panel.clone()) + } +} diff --git a/crates/assistant_tooling/src/registry.rs b/crates/assistant_tooling/src/registry.rs index 8c969c0d80..ac5930cac4 100644 --- a/crates/assistant_tooling/src/registry.rs +++ b/crates/assistant_tooling/src/registry.rs @@ -256,7 +256,7 @@ mod test { let expected = ToolFunctionDefinition { name: "get_current_weather".to_string(), description: "Fetches the current weather for a given location.".to_string(), - parameters: schema_for!(WeatherQuery).schema, + parameters: schema_for!(WeatherQuery), }; assert_eq!(tools[0].name, expected.name); @@ -267,6 +267,7 @@ mod test { assert_eq!( expected_schema, json!({ + "$schema": "http://json-schema.org/draft-07/schema#", "title": "WeatherQuery", "type": "object", "properties": { diff --git a/crates/assistant_tooling/src/tool.rs b/crates/assistant_tooling/src/tool.rs index b63e2901c6..a3b021a04e 100644 --- a/crates/assistant_tooling/src/tool.rs +++ b/crates/assistant_tooling/src/tool.rs @@ -1,8 +1,11 @@ use anyhow::Result; use gpui::{div, AnyElement, AppContext, Element, ParentElement as _, Task, WindowContext}; -use schemars::{schema::SchemaObject, schema_for, JsonSchema}; +use schemars::{schema::RootSchema, schema_for, JsonSchema}; use serde::Deserialize; -use std::{any::Any, fmt::Debug}; +use std::{ + any::Any, + fmt::{Debug, Display}, +}; #[derive(Default, Deserialize)] pub struct ToolFunctionCall { @@ -89,7 +92,17 @@ impl ToolFunctionCallResult { pub struct ToolFunctionDefinition { pub name: String, pub description: String, - pub parameters: SchemaObject, + pub parameters: RootSchema, +} + +impl Display for ToolFunctionDefinition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let schema = serde_json::to_string(&self.parameters).ok(); + let schema = schema.unwrap_or("None".to_string()); + write!(f, "Name: {}:\n", self.name)?; + write!(f, "Description: {}\n", self.description)?; + write!(f, "Parameters: {}", schema) + } } impl Debug for ToolFunctionDefinition { @@ -124,10 +137,12 @@ pub trait LanguageModelTool { /// The OpenAI Function definition for the tool, for direct use with OpenAI's API. fn definition(&self) -> ToolFunctionDefinition { + let root_schema = schema_for!(Self::Input); + ToolFunctionDefinition { name: self.name(), description: self.description(), - parameters: schema_for!(Self::Input).schema, + parameters: root_schema, } } From fbc6e930a77e9fa5c951b6ae0bd61d0a7c31d96d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Apr 2024 10:18:52 +0200 Subject: [PATCH 032/101] Fix regressions in `List` (#10924) Release Notes: - N/A --- crates/gpui/src/elements/list.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 784955a5d7..a6f65e9469 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -202,13 +202,14 @@ impl ListState { /// /// Note that this will cause scroll events to be dropped until the next paint. pub fn reset(&self, element_count: usize) { - { + let old_count = { let state = &mut *self.0.borrow_mut(); state.reset = true; state.logical_scroll_top = None; - } + state.items.summary().count + }; - self.splice(0..element_count, element_count); + self.splice(0..old_count, element_count); } /// The number of items in this list. @@ -613,6 +614,9 @@ impl StateInner { let mut layout_response = self.layout_items(Some(bounds.size.width), bounds.size.height, &padding, cx); + // Avoid honoring autoscroll requests from elements other than our children. + cx.take_autoscroll(); + // Only paint the visible items, if there is actually any space for them (taking padding into account) if bounds.size.height > padding.top + padding.bottom { let mut item_origin = bounds.origin + Point::new(px(0.), padding.top); From dfd4d2a4371f1226df313b621cd578fc597dc160 Mon Sep 17 00:00:00 2001 From: hardlydearly <167623323+hardlydearly@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:34:32 +0800 Subject: [PATCH 033/101] chore: remove repetitive word (#10923) --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 54bbdc8f0c..0d2a811ccd 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1497,7 +1497,7 @@ fn task_summary(task: &TaskState, error_code: Option) -> (String, String) { /// * ignores `\n` and \r` character input, requiring the `newline` call instead /// /// * does not alter grid state after `newline` call -/// so its `bottommost_line` is always the the same additions, and +/// so its `bottommost_line` is always the same additions, and /// the cursor's `point` is not updated to the new line and column values /// /// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. From 135a5f211456e0f648e1fc364d78548043097ab0 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 24 Apr 2024 16:44:00 +0800 Subject: [PATCH 034/101] Enable unfocused windows to update their status based on whether they are clickable or not (#10229) - Fixed #9784 By removing the interception of the MouseMove event, zed can update the corresponding Hover even when it is inactive --- crates/gpui/src/platform/mac/window.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 793caca8d2..bcabaa786f 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -340,7 +340,6 @@ struct MacWindowState { native_view: NonNull, display_link: Option, renderer: renderer::Renderer, - kind: WindowKind, request_frame_callback: Option>, event_callback: Option crate::DispatchEventResult>>, activate_callback: Option>, @@ -633,7 +632,6 @@ impl MacWindow { native_view as *mut _, window_size, ), - kind, request_frame_callback: None, event_callback: None, activate_callback: None, @@ -1343,7 +1341,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let window_state = unsafe { get_window_state(this) }; let weak_window_state = Arc::downgrade(&window_state); let mut lock = window_state.as_ref().lock(); - let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; let window_height = lock.content_size().height; let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; @@ -1429,8 +1426,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { } } - PlatformInput::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return, - PlatformInput::MouseUp(MouseUpEvent { .. }) => { lock.synthetic_drag_counter += 1; } From 6108140a02c778bfc471cc102f5afdbc1a5934df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 24 Apr 2024 14:12:59 +0300 Subject: [PATCH 035/101] Properly extract package name out of cargo pkgid (#10929) Fixes https://github.com/zed-industries/zed/issues/10925 Uses correct package name to generate Rust `cargo` tasks. Also deduplicates lines in task modal item tooltips. Release Notes: - Fixed Rust tasks using incorrect package name ([10925](https://github.com/zed-industries/zed/issues/10925)) --- crates/languages/src/rust.rs | 70 +++++++++++++++++++++++++----------- crates/tasks_ui/src/modal.rs | 4 ++- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 0b8e449f49..084af44120 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -421,13 +421,6 @@ impl ContextProvider for RustContextProvider { } fn human_readable_package_name(package_directory: &Path) -> Option { - fn split_off_suffix(input: &str, suffix_start: char) -> &str { - match input.rsplit_once(suffix_start) { - Some((without_suffix, _)) => without_suffix, - None => input, - } - } - let pkgid = String::from_utf8( std::process::Command::new("cargo") .current_dir(package_directory) @@ -437,19 +430,40 @@ fn human_readable_package_name(package_directory: &Path) -> Option { .stdout, ) .ok()?; - // For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned. - // Output example in the root of Zed project: - // ```bash - // ❯ cargo pkgid zed - // path+file:///absolute/path/to/project/zed/crates/zed#0.131.0 - // ``` - // Extrarct the package name from the output according to the spec: - // https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar - let mut package_name = pkgid.trim(); - package_name = split_off_suffix(package_name, '#'); - package_name = split_off_suffix(package_name, '?'); - let (_, package_name) = package_name.rsplit_once('/')?; - Some(package_name.to_string()) + Some(package_name_from_pkgid(&pkgid)?.to_owned()) +} + +// For providing local `cargo check -p $pkgid` task, we do not need most of the information we have returned. +// Output example in the root of Zed project: +// ```bash +// ❯ cargo pkgid zed +// path+file:///absolute/path/to/project/zed/crates/zed#0.131.0 +// ``` +// Another variant, if a project has a custom package name or hyphen in the name: +// ``` +// path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0 +// ``` +// +// Extracts the package name from the output according to the spec: +// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#specification-grammar +fn package_name_from_pkgid(pkgid: &str) -> Option<&str> { + fn split_off_suffix(input: &str, suffix_start: char) -> &str { + match input.rsplit_once(suffix_start) { + Some((without_suffix, _)) => without_suffix, + None => input, + } + } + + let (version_prefix, version_suffix) = pkgid.trim().rsplit_once('#')?; + let package_name = match version_suffix.rsplit_once('@') { + Some((custom_package_name, _version)) => custom_package_name, + None => { + let host_and_path = split_off_suffix(version_prefix, '?'); + let (_, package_name) = host_and_path.rsplit_once('/')?; + package_name + } + }; + Some(package_name) } async fn get_cached_server_binary(container_dir: PathBuf) -> Option { @@ -750,4 +764,20 @@ mod tests { buffer }); } + + #[test] + fn test_package_name_from_pkgid() { + for (input, expected) in [ + ( + "path+file:///absolute/path/to/project/zed/crates/zed#0.131.0", + "zed", + ), + ( + "path+file:///absolute/path/to/project/custom-package#my-custom-package@0.1.0", + "my-custom-package", + ), + ] { + assert_eq!(package_name_from_pkgid(input), Some(expected)); + } + } } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d63b56b015..156eab497f 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -313,7 +313,9 @@ impl PickerDelegate for TasksModalDelegate { String::new() }; if let Some(resolved) = resolved_task.resolved.as_ref() { - if display_label != resolved.command_label { + if resolved.command_label != display_label + && resolved.command_label != resolved_task.resolved_label + { if !tooltip_label_text.trim().is_empty() { tooltip_label_text.push('\n'); } From f7ea1370a454311b9635f50c1e06b5d508d56d88 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 24 Apr 2024 14:01:10 +0200 Subject: [PATCH 036/101] Update docstring for SumTree (#10927) Need the updated docstring for the blog post. Release Notes: - N/A --- crates/sum_tree/src/sum_tree.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 3909ef1b39..6b32a80445 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -151,8 +151,10 @@ impl Bias { } } -/// A B-tree where each leaf node contains an [`Item`] of type `T`, -/// and each internal node contains a [`Summary`] of the items in its subtree. +/// A B+ tree in which each leaf node contains `Item`s of type `T` and a `Summary`s for each `Item`. +/// Each internal node contains a `Summary` of the items in its subtree. +/// +/// The maximum number of items per node is `TREE_BASE * 2`. /// /// Any [`Dimension`] supported by the [`Summary`] type can be used to seek to a specific location in the tree. #[derive(Debug, Clone)] From 25e239d986234ea27812e15c8e71f23b55616f32 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Apr 2024 14:12:44 +0200 Subject: [PATCH 037/101] Fix autoscroll in the new assistant (#10928) This removes the manual calls to `scroll_to_reveal_item` in the new assistant, as they are superseded by the new autoscrolling behavior of the `List` when the editor requests one. Release Notes: - N/A --- crates/assistant2/src/assistant2.rs | 24 +------------- crates/gpui/src/elements/list.rs | 49 ++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 5a9d6c8df6..683ce911af 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use assistant_tooling::{ToolFunctionCall, ToolRegistry}; use client::{proto, Client}; use completion_provider::*; -use editor::{Editor, EditorEvent}; +use editor::Editor; use feature_flags::FeatureFlagAppExt as _; use futures::{channel::oneshot, future::join_all, Future, FutureExt, StreamExt}; use gpui::{ @@ -426,31 +426,10 @@ impl AssistantChat { } editor }); - let _subscription = cx.subscribe(&body, move |this, editor, event, cx| match event { - EditorEvent::SelectionsChanged { .. } => { - if editor.read(cx).is_focused(cx) { - let (message_ix, _message) = this - .messages - .iter() - .enumerate() - .find_map(|(ix, message)| match message { - ChatMessage::User(user_message) if user_message.id == id => { - Some((ix, user_message)) - } - _ => None, - }) - .expect("user message not found"); - - this.list_state.scroll_to_reveal_item(message_ix); - } - } - _ => {} - }); let message = ChatMessage::User(UserMessage { id, body, contexts: Vec::new(), - _subscription, }); self.push_message(message, cx); } @@ -733,7 +712,6 @@ struct UserMessage { id: MessageId, body: View, contexts: Vec, - _subscription: gpui::Subscription, } struct AssistantMessage { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index a6f65e9469..d5caf22955 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -608,6 +608,7 @@ impl StateInner { &mut self, bounds: Bounds, padding: Edges, + autoscroll: bool, cx: &mut ElementContext, ) -> Result { cx.transact(|cx| { @@ -627,11 +628,45 @@ impl StateInner { }); if let Some(autoscroll_bounds) = cx.take_autoscroll() { - if bounds.intersect(&autoscroll_bounds) != autoscroll_bounds { - return Err(ListOffset { - item_ix: item.index, - offset_in_item: autoscroll_bounds.origin.y - item_origin.y, - }); + if autoscroll { + if autoscroll_bounds.top() < bounds.top() { + return Err(ListOffset { + item_ix: item.index, + offset_in_item: autoscroll_bounds.top() - item_origin.y, + }); + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::(); + cursor.seek(&Count(item.index), Bias::Right, &()); + let mut height = bounds.size.height - padding.top - padding.bottom; + + // Account for the height of the element down until the autoscroll bottom. + height -= autoscroll_bounds.bottom() - item_origin.y; + + // Keep decreasing the scroll top until we fill all the available space. + while height > Pixels::ZERO { + cursor.prev(&()); + let Some(item) = cursor.item() else { break }; + + let size = item.size().unwrap_or_else(|| { + let mut item = (self.render_item)(cursor.start().0, cx); + let item_available_size = size( + bounds.size.width.into(), + AvailableSpace::MinContent, + ); + item.layout_as_root(item_available_size, cx) + }); + height -= size.height; + } + + return Err(ListOffset { + item_ix: cursor.start().0, + offset_in_item: if height < Pixels::ZERO { + -height + } else { + Pixels::ZERO + }, + }); + } } } @@ -762,11 +797,11 @@ impl Element for List { } let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - let layout = match state.prepaint_items(bounds, padding, cx) { + let layout = match state.prepaint_items(bounds, padding, true, cx) { Ok(layout) => layout, Err(autoscroll_request) => { state.logical_scroll_top = Some(autoscroll_request); - state.prepaint_items(bounds, padding, cx).unwrap() + state.prepaint_items(bounds, padding, false, cx).unwrap() } }; From e1791b7dd0ff1948d3d302674fda2f659974c0b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 24 Apr 2024 14:33:57 +0200 Subject: [PATCH 038/101] Autoscroll containing element when editor has a pending selection (#10931) Release Notes: - N/A --- crates/editor/src/element.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49917d7ade..b886c5db1f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3501,10 +3501,11 @@ impl Element for EditorElement { let content_origin = text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO); - let mut autoscroll_requested = false; + let mut autoscroll_containing_element = false; let mut autoscroll_horizontally = false; self.editor.update(cx, |editor, cx| { - autoscroll_requested = editor.autoscroll_requested(); + autoscroll_containing_element = + editor.autoscroll_requested() || editor.has_pending_selection(); autoscroll_horizontally = editor.autoscroll_vertically(bounds, line_height, cx); snapshot = editor.snapshot(cx); }); @@ -3683,7 +3684,7 @@ impl Element for EditorElement { scroll_pixel_position, line_height, em_width, - autoscroll_requested, + autoscroll_containing_element, cx, ); From 76ff4679651d3190f1df1e9510befb0b0f4805fd Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 11:22:42 -0400 Subject: [PATCH 039/101] Log which language servers will be started (#10936) This PR adds a new log message indicating which language servers will be started for a given language. The aim is to make debugging the usage of the new `language_servers` setting (#10911) easier. Release Notes: - N/A --- crates/project/src/project.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 019fca5ca2..9bbcce6f6f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3068,12 +3068,21 @@ impl Project { let enabled_language_servers = settings.customized_language_servers(&available_language_servers); + let enabled_lsp_adapters = available_lsp_adapters + .into_iter() + .filter(|adapter| enabled_language_servers.contains(&adapter.name)) + .collect::>(); - for adapter in available_lsp_adapters { - if !enabled_language_servers.contains(&adapter.name) { - continue; - } + log::info!( + "starting language servers for {language}: {adapters}", + language = language.name(), + adapters = enabled_lsp_adapters + .iter() + .map(|adapter| adapter.name.0.as_ref()) + .join(", ") + ); + for adapter in enabled_lsp_adapters { self.start_language_server(worktree, adapter.clone(), language.clone(), cx); } } From d0a5dbd8cbefc39bf3e0e8ad7c62fcca40d34e48 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 11:42:18 -0400 Subject: [PATCH 040/101] terraform: Sync `Cargo.toml` version with `extension.toml` version (#10937) This PR syncs the `Cargo.toml` version with the `extension.toml` version. We should try to keep these in sync. Release Notes: - N/A --- Cargo.lock | 2 +- extensions/terraform/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0caa43fc13..60b196627e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12858,7 +12858,7 @@ dependencies = [ [[package]] name = "zed_terraform" -version = "0.0.2" +version = "0.0.3" dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/extensions/terraform/Cargo.toml b/extensions/terraform/Cargo.toml index c8a39f1092..7225ece8d9 100644 --- a/extensions/terraform/Cargo.toml +++ b/extensions/terraform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_terraform" -version = "0.0.2" +version = "0.0.3" edition = "2021" publish = false license = "Apache-2.0" From d8437136c747336d21d4803b2a80de023887cc88 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 12:42:33 -0400 Subject: [PATCH 041/101] Fix primary language server selection for formatting (#10939) This PR fixes the way we select the primary language server for use with formatting. Previously we were just taking the first one in the list, but this could be the wrong one in cases where a language server was provided by an extension in conjunction with a built-in language server (e.g., Tailwind). We now use the `primary_language_server_for_buffer` method to more accurately identify the primary one. Fixes https://github.com/zed-industries/zed/issues/10902. Release Notes: - Fixed an issue where the wrong language server could be used for formatting. --- crates/project/src/project.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9bbcce6f6f..444393f17c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4676,12 +4676,21 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path) in &buffers_with_paths { - let adapters_and_servers: Vec<_> = project.update(&mut cx, |project, cx| { - project - .language_servers_for_buffer(&buffer.read(cx), cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect() - })?; + let (primary_adapter_and_server, adapters_and_servers) = + project.update(&mut cx, |project, cx| { + let buffer = buffer.read(cx); + + let adapters_and_servers = project + .language_servers_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect::>(); + + let primary_adapter = project + .primary_language_server_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())); + + (primary_adapter, adapters_and_servers) + })?; let settings = buffer.update(&mut cx, |buffer, cx| { language_settings(buffer.language(), buffer.file(), cx).clone() @@ -4734,10 +4743,8 @@ impl Project { // Apply language-specific formatting using either the primary language server // or external command. // Except for code actions, which are applied with all connected language servers. - let primary_language_server = adapters_and_servers - .first() - .cloned() - .map(|(_, lsp)| lsp.clone()); + let primary_language_server = + primary_adapter_and_server.map(|(_adapter, server)| server.clone()); let server_and_buffer = primary_language_server .as_ref() .zip(buffer_abs_path.as_ref()); From facd04c902688af1609c080d312bd20ac3949817 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 24 Apr 2024 12:46:30 -0400 Subject: [PATCH 042/101] v0.134.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 60b196627e..2525afc8ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12607,7 +12607,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.133.0" +version = "0.134.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2eb188f768..6f039a77a5 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.133.0" +version = "0.134.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] From bd77232f650cb650101d546262d9106b97ab7771 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 13:03:56 -0400 Subject: [PATCH 043/101] dart: Bump to v0.0.2 (#10940) This PR bumps the Dart extension to v0.0.2. Changes: - https://github.com/zed-industries/zed/pull/8347 - https://github.com/zed-industries/zed/pull/10552 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/dart/Cargo.toml | 2 +- extensions/dart/extension.toml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2525afc8ec..30999e233d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12724,7 +12724,7 @@ dependencies = [ [[package]] name = "zed_dart" -version = "0.0.1" +version = "0.0.2" dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/extensions/dart/Cargo.toml b/extensions/dart/Cargo.toml index 4cee6ba3af..e64053f44f 100644 --- a/extensions/dart/Cargo.toml +++ b/extensions/dart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_dart" -version = "0.0.1" +version = "0.0.2" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/dart/extension.toml b/extensions/dart/extension.toml index f13851b3dc..e9170c785b 100644 --- a/extensions/dart/extension.toml +++ b/extensions/dart/extension.toml @@ -1,7 +1,7 @@ id = "dart" name = "Dart" description = "Dart support." -version = "0.0.1" +version = "0.0.2" schema_version = 1 authors = ["Abdullah Alsigar ", "Flo "] repository = "https://github.com/zed-industries/zed" @@ -9,6 +9,7 @@ repository = "https://github.com/zed-industries/zed" [language_servers.dart] name = "Dart LSP" language = "Dart" +languages = ["Dart"] [grammars.dart] repository = "https://github.com/UserNobody14/tree-sitter-dart" From 048fc7ad098f2e99ecf6e09e067c8c425955d3b8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 24 Apr 2024 13:15:19 -0600 Subject: [PATCH 044/101] Allow cli to accept --dev-server-token (#10944) Release Notes: - N/A --- Cargo.lock | 1 + crates/cli/src/main.rs | 28 ++++++++++++++++++++++++++-- crates/headless/Cargo.toml | 1 + crates/headless/src/headless.rs | 7 +++++-- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30999e233d..b28d6fd4a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4724,6 +4724,7 @@ dependencies = [ "project", "rpc", "settings", + "shellexpand", "util", ] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1f4568e0f9..12440819d0 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -36,6 +36,9 @@ struct Args { /// Custom Zed.app path #[arg(short, long)] bundle_path: Option, + /// Run zed in dev-server mode + #[arg(long)] + dev_server_token: Option, } fn parse_path_with_position( @@ -67,6 +70,10 @@ fn main() -> Result<()> { let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?; + if let Some(dev_server_token) = args.dev_server_token { + return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]); + } + if args.version { println!("{}", bundle.zed_version_string()); return Ok(()); @@ -169,6 +176,10 @@ mod linux { unimplemented!() } + pub fn spawn(&self, _args: Vec) -> anyhow::Result<()> { + unimplemented!() + } + pub fn zed_version_string(&self) -> String { unimplemented!() } @@ -202,6 +213,10 @@ mod windows { unimplemented!() } + pub fn spawn(&self, _args: Vec) -> anyhow::Result<()> { + unimplemented!() + } + pub fn zed_version_string(&self) -> String { unimplemented!() } @@ -217,7 +232,7 @@ mod mac_os { url::{CFURLCreateWithBytes, CFURL}, }; use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType}; - use std::{fs, path::Path, ptr}; + use std::{fs, path::Path, process::Command, ptr}; use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME}; use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; @@ -278,6 +293,15 @@ mod mac_os { } } + pub fn spawn(&self, args: Vec) -> Result<()> { + let path = match self { + Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"), + Self::LocalPath { executable, .. } => executable.clone(), + }; + Command::new(path).args(args).status()?; + Ok(()) + } + pub fn launch(&self) -> anyhow::Result<(IpcSender, IpcReceiver)> { let (server, server_name) = IpcOneShotServer::::new().context("Handshake before Zed spawn")?; @@ -358,12 +382,12 @@ mod mac_os { ) } } + pub(super) fn spawn_channel_cli( channel: release_channel::ReleaseChannel, leftover_args: Vec, ) -> Result<()> { use anyhow::bail; - use std::process::Command; let app_id_prompt = format!("id of app \"{}\"", channel.display_name()); let app_id_output = Command::new("osascript") diff --git a/crates/headless/Cargo.toml b/crates/headless/Cargo.toml index 772f625a6f..28a213f79e 100644 --- a/crates/headless/Cargo.toml +++ b/crates/headless/Cargo.toml @@ -26,6 +26,7 @@ project.workspace = true fs.workspace = true futures.workspace = true settings.workspace = true +shellexpand.workspace = true postage.workspace = true [dev-dependencies] diff --git a/crates/headless/src/headless.rs b/crates/headless/src/headless.rs index 13e6cbb9fa..dd31360f91 100644 --- a/crates/headless/src/headless.rs +++ b/crates/headless/src/headless.rs @@ -180,7 +180,8 @@ impl DevServer { _: Arc, cx: AsyncAppContext, ) -> Result { - let path = std::path::Path::new(&envelope.payload.path); + let expanded = shellexpand::tilde(&envelope.payload.path).to_string(); + let path = std::path::Path::new(&expanded); let fs = cx.read_model(&this, |this, _| this.app_state.fs.clone())?; let path_exists = fs.is_dir(path).await; @@ -232,9 +233,11 @@ impl DevServer { (this.client.clone(), project) })?; + let path = shellexpand::tilde(&remote_project.path).to_string(); + project .update(cx, |project, cx| { - project.find_or_create_local_worktree(&remote_project.path, true, cx) + project.find_or_create_local_worktree(&path, true, cx) })? .await?; From 9e88155a4860b73e9175acbd3e9a4305ceb95f3d Mon Sep 17 00:00:00 2001 From: Maksim Bondarenkov <119937608+ognevny@users.noreply.github.com> Date: Wed, 24 Apr 2024 22:59:18 +0300 Subject: [PATCH 045/101] Use winresource instead of embed-manifest (#10810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use winresource for crates/zed and crates/storybook. tested on `x86_64-pc-windows-gnu`. on `x86_64-pc-windows-msvc` I receive a error message, that looks like a problem with my machine   Release Notes: - N/A --- Cargo.lock | 9 +-------- crates/storybook/Cargo.toml | 2 +- crates/storybook/build.rs | 10 ++++++++-- crates/zed/Cargo.toml | 1 - crates/zed/build.rs | 5 ++--- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b28d6fd4a0..f980ce11fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3433,12 +3433,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "embed-manifest" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae" - [[package]] name = "emojis" version = "0.6.1" @@ -9489,7 +9483,6 @@ dependencies = [ "ctrlc", "dialoguer", "editor", - "embed-manifest", "fuzzy", "gpui", "indoc", @@ -9505,6 +9498,7 @@ dependencies = [ "strum", "theme", "ui", + "winresource", ] [[package]] @@ -12634,7 +12628,6 @@ dependencies = [ "db", "diagnostics", "editor", - "embed-manifest", "env_logger", "extension", "extensions_ui", diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 5b19171349..b7fae8ebbc 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -36,7 +36,7 @@ theme.workspace = true ui = { workspace = true, features = ["stories"] } [target.'cfg(target_os = "windows")'.build-dependencies] -embed-manifest = "1.4.0" +winresource = "0.1" [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/build.rs b/crates/storybook/build.rs index fcb9db5cb7..d165aee5d5 100644 --- a/crates/storybook/build.rs +++ b/crates/storybook/build.rs @@ -12,7 +12,13 @@ fn main() { let manifest = std::path::Path::new("../zed/resources/windows/manifest.xml"); println!("cargo:rerun-if-changed={}", manifest.display()); - embed_manifest::embed_manifest(embed_manifest::new_manifest(manifest.to_str().unwrap())) - .unwrap(); + + let mut res = winresource::WindowsResource::new(); + res.set_manifest_file(manifest.to_str().unwrap()); + + if let Err(e) = res.compile() { + eprintln!("{}", e); + std::process::exit(1); + } } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f039a77a5..248b9488d4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -96,7 +96,6 @@ workspace.workspace = true zed_actions.workspace = true [target.'cfg(target_os = "windows")'.build-dependencies] -embed-manifest = "1.4.0" winresource = "0.1" [dev-dependencies] diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 8def598fd4..a1126afed7 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -57,11 +57,10 @@ fn main() { println!("cargo:rerun-if-changed={}", manifest.display()); println!("cargo:rerun-if-changed={}", icon.display()); - embed_manifest::embed_manifest(embed_manifest::new_manifest(manifest.to_str().unwrap())) - .unwrap(); - let mut res = winresource::WindowsResource::new(); res.set_icon(icon.to_str().unwrap()); + res.set_manifest_file(manifest.to_str().unwrap()); + if let Err(e) = res.compile() { eprintln!("{}", e); std::process::exit(1); From 06d2d9da5fbf7fe2f0dff4d5b4a9de48049729db 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, 25 Apr 2024 04:00:25 +0800 Subject: [PATCH 046/101] windows: Let the OS decide which font to use as the UI font (#10877) On my computer, I get `Yahei UI`, which makes sense since I'm using a Chinese operating system, and `Yahei UI` includes Chinese codepoints. On an English operating system, `Segoe UI` should be used instead. Edit: I also choose to use the UI font selected by the system as the fallback font, rather than hard-coding the `Arial` font. Release Notes: - N/A --- .../gpui/src/platform/windows/direct_write.rs | 106 +++++++++++------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index d536a6c4d0..96b95114c6 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -16,9 +16,11 @@ use windows::{ Direct2D::{Common::*, *}, DirectWrite::*, Dxgi::Common::*, + Gdi::LOGFONTW, Imaging::{D2D::IWICImagingFactory2, *}, }, System::{Com::*, SystemServices::LOCALE_NAME_MAX_LENGTH}, + UI::WindowsAndMessaging::*, }, }; @@ -51,6 +53,7 @@ unsafe impl Send for DirectWriteComponent {} struct DirectWriteState { components: DirectWriteComponent, + system_ui_font_name: SharedString, system_font_collection: IDWriteFontCollection1, custom_font_collection: IDWriteFontCollection1, fonts: Vec, @@ -106,9 +109,11 @@ impl DirectWriteTextSystem { .factory .CreateFontCollectionFromFontSet(&custom_font_set)? }; + let system_ui_font_name = get_system_ui_font_name(); Ok(Self(RwLock::new(DirectWriteState { components, + system_ui_font_name, system_font_collection, custom_font_collection, fonts: Vec::new(), @@ -309,53 +314,55 @@ impl DirectWriteState { } fn select_font(&mut self, target_font: &Font) -> FontId { - let family_name = if target_font.family == ".SystemUIFont" { - // https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts - // Segoe UI is the Windows font intended for user interface text strings. - "Segoe UI" - } else { - target_font.family.as_ref() - }; unsafe { - // try to find target font in custom font collection first - self.get_font_id_from_font_collection( - family_name, - target_font.weight, - target_font.style, - &target_font.features, - false, - ) - .or_else(|| { - self.get_font_id_from_font_collection( - family_name, + if target_font.family == ".SystemUIFont" { + let family = self.system_ui_font_name.clone(); + self.find_font_id( + family.as_ref(), target_font.weight, target_font.style, &target_font.features, - true, ) + .unwrap() + } else { + self.find_font_id( + target_font.family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + ) + .unwrap_or_else(|| { + let family = self.system_ui_font_name.clone(); + log::error!("{} not found, use {} instead.", target_font.family, family); + self.get_font_id_from_font_collection( + family.as_ref(), + target_font.weight, + target_font.style, + &target_font.features, + true, + ) + .unwrap() + }) + } + } + } + + unsafe fn find_font_id( + &mut self, + family_name: &str, + weight: FontWeight, + style: FontStyle, + features: &FontFeatures, + ) -> Option { + // try to find target font in custom font collection first + self.get_font_id_from_font_collection(family_name, weight, style, features, false) + .or_else(|| { + self.get_font_id_from_font_collection(family_name, weight, style, features, true) }) .or_else(|| { self.update_system_font_collection(); - self.get_font_id_from_font_collection( - family_name, - target_font.weight, - target_font.style, - &target_font.features, - true, - ) + self.get_font_id_from_font_collection(family_name, weight, style, features, true) }) - .or_else(|| { - log::error!("{} not found, use Arial instead.", family_name); - self.get_font_id_from_font_collection( - "Arial", - target_font.weight, - target_font.style, - &target_font.features, - false, - ) - }) - .unwrap() - } } fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { @@ -1271,6 +1278,29 @@ fn translate_color(color: &DWRITE_COLOR_F) -> D2D1_COLOR_F { } } +fn get_system_ui_font_name() -> SharedString { + unsafe { + let mut info: LOGFONTW = std::mem::zeroed(); + let font_family = if SystemParametersInfoW( + SPI_GETICONTITLELOGFONT, + std::mem::size_of::() as u32, + Some(&mut info as *mut _ as _), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ) + .log_err() + .is_none() + { + // https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-fonts + // Segoe UI is the Windows font intended for user interface text strings. + "Segoe UI".into() + } else { + String::from_utf16_lossy(&info.lfFaceName).into() + }; + log::info!("Use {} as UI font.", font_family); + font_family + } +} + const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F { r: 1.0, From 53f67a82414fa0429fe8f8bdc2098700f65aa4b6 Mon Sep 17 00:00:00 2001 From: Dzmitry Malyshau Date: Wed, 24 Apr 2024 13:02:11 -0700 Subject: [PATCH 047/101] Update blade with transparency and exclusive fullscreen fixes (#10880) Release Notes: - N/A Picks up https://github.com/kvark/blade/pull/113 and a bunch of other fixes. Should prevent the exclusive full-screen on Vulkan - related to #9728 cc @kazatsuyu Note: this PR doesn't enable transparency, this is left to follow-up --- Cargo.lock | 29 ++++++----- Cargo.toml | 5 +- crates/gpui/Cargo.toml | 4 +- .../gpui/src/platform/blade/blade_renderer.rs | 49 +++++++++---------- .../gpui/src/platform/linux/wayland/window.rs | 36 ++++++-------- crates/gpui/src/platform/linux/x11/window.rs | 37 +++++--------- crates/gpui/src/platform/mac/window.rs | 27 +++++----- crates/gpui/src/platform/windows/window.rs | 46 +++++++++-------- 8 files changed, 104 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f980ce11fc..34f4720b75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -284,21 +284,21 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "ash" -version = "0.37.3+1.3.251" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.0", ] [[package]] name = "ash-window" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b912285a7c29f3a8f87ca6f55afc48768624e5e33ec17dbd2f2075903f5e35ab" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" dependencies = [ "ash", - "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", "raw-window-metal", ] @@ -1479,7 +1479,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.4.0" -source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4" +source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3" dependencies = [ "ash", "ash-window", @@ -1500,7 +1500,7 @@ dependencies = [ "mint", "naga", "objc", - "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", "slab", "wasm-bindgen", "web-sys", @@ -1509,7 +1509,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.2.1" -source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4" +source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3" dependencies = [ "proc-macro2", "quote", @@ -4486,9 +4486,9 @@ dependencies = [ [[package]] name = "gpu-alloc-ash" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2424bc9be88170e1a56e57c25d3d0e2dfdd22e8f328e892786aeb4da1415732" +checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a" dependencies = [ "ash", "gpu-alloc-types", @@ -4555,7 +4555,6 @@ dependencies = [ "postage", "profiling", "rand 0.8.5", - "raw-window-handle 0.5.2", "raw-window-handle 0.6.0", "refineable", "resvg", @@ -7726,14 +7725,14 @@ checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" [[package]] name = "raw-window-metal" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4ea493258d54c24cb46aa9345d099e58e2ea3f30dd63667fc54fc892f18e76" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" dependencies = [ "cocoa", "core-graphics", "objc", - "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7a883e4586..5e2c1b27c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -249,9 +249,8 @@ async-recursion = "1.0.0" async-tar = "0.4.2" async-trait = "0.1" bitflags = "2.4.2" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "810ec594358aafea29a4a3d8ab601d25292b2ce4" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "810ec594358aafea29a4a3d8ab601d25292b2ce4" } -blade-rwh = { package = "raw-window-handle", version = "0.5" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" } cap-std = "3.0" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index f49ab6e571..9198c99b7e 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -14,7 +14,7 @@ workspace = true default = [] test-support = ["backtrace", "collections/test-support", "util/test-support"] runtime_shaders = [] -macos-blade = ["blade-graphics", "blade-macros", "blade-rwh", "bytemuck"] +macos-blade = ["blade-graphics", "blade-macros", "bytemuck"] [lib] path = "src/gpui.rs" @@ -26,7 +26,6 @@ async-task = "4.7" backtrace = { version = "0.3", optional = true } blade-graphics = { workspace = true, optional = true } blade-macros = { workspace = true, optional = true } -blade-rwh = { workspace = true, optional = true } bytemuck = { version = "1", optional = true } collections.workspace = true ctor.workspace = true @@ -95,7 +94,6 @@ flume = "0.11" #TODO: use these on all platforms blade-graphics.workspace = true blade-macros.workspace = true -blade-rwh.workspace = true bytemuck = "1" cosmic-text = "0.11.2" copypasta = "0.10.1" diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index ae7dda2f92..56234058f5 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -12,7 +12,7 @@ use collections::HashMap; #[cfg(target_os = "macos")] use media::core_video::CVMetalTextureCache; #[cfg(target_os = "macos")] -use std::ffi::c_void; +use std::{ffi::c_void, ptr::NonNull}; use blade_graphics as gpu; use std::{mem, sync::Arc}; @@ -25,35 +25,32 @@ pub type Renderer = BladeRenderer; #[cfg(target_os = "macos")] pub unsafe fn new_renderer( _context: self::Context, - native_window: *mut c_void, + _native_window: *mut c_void, native_view: *mut c_void, bounds: crate::Size, ) -> Renderer { + use raw_window_handle as rwh; struct RawWindow { - window: *mut c_void, view: *mut c_void, } - unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { - fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { - let mut wh = blade_rwh::AppKitWindowHandle::empty(); - wh.ns_window = self.window; - wh.ns_view = self.view; - wh.into() + impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result { + let view = NonNull::new(self.view).unwrap(); + let handle = rwh::AppKitWindowHandle::new(view); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) } } - - unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { - fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle { - let dh = blade_rwh::AppKitDisplayHandle::empty(); - dh.into() + impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result { + let handle = rwh::AppKitDisplayHandle::new(); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } } let gpu = Arc::new( gpu::Context::init_windowed( &RawWindow { - window: native_window as *mut _, view: native_view as *mut _, }, gpu::ContextDesc { @@ -184,7 +181,7 @@ struct BladePipelines { } impl BladePipelines { - fn new(gpu: &gpu::Context, surface_format: gpu::TextureFormat) -> Self { + fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo) -> Self { use gpu::ShaderData as _; let shader = gpu.create_shader(gpu::ShaderDesc { @@ -216,7 +213,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_quad"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -233,7 +230,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_shadow"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -267,7 +264,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_path"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -284,7 +281,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_underline"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -301,7 +298,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_mono_sprite"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -318,7 +315,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_poly_sprite"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -335,7 +332,7 @@ impl BladePipelines { depth_stencil: None, fragment: shader.at("fs_surface"), color_targets: &[gpu::ColorTargetState { - format: surface_format, + format: surface_info.format, blend: Some(gpu::BlendState::ALPHA_BLENDING), write_mask: gpu::ColorWrites::default(), }], @@ -367,16 +364,18 @@ impl BladeRenderer { //Note: this matches the original logic of the Metal backend, // but ultimaterly we need to switch to `Linear`. color_space: gpu::ColorSpace::Srgb, + allow_exclusive_full_screen: false, + transparent: false, } } pub fn new(gpu: Arc, size: gpu::Extent) -> Self { - let surface_format = gpu.resize(Self::make_surface_config(size)); + let surface_info = gpu.resize(Self::make_surface_config(size)); let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc { name: "main", buffer_count: 2, }); - let pipelines = BladePipelines::new(&gpu, surface_format); + let pipelines = BladePipelines::new(&gpu, surface_info); let instance_belt = BladeBelt::new(BladeBeltDescriptor { memory: gpu::Memory::Shared, min_chunk_size: 0x1000, diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 56f2b876b8..2b1b8d1c7f 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -2,16 +2,14 @@ use std::any::Any; use std::cell::{Ref, RefCell, RefMut}; use std::ffi::c_void; use std::num::NonZeroU32; +use std::ptr::NonNull; use std::rc::{Rc, Weak}; use std::sync::Arc; use blade_graphics as gpu; -use blade_rwh::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle}; use collections::{HashMap, HashSet}; use futures::channel::oneshot::Receiver; -use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle, -}; +use raw_window_handle as rwh; use wayland_backend::client::ObjectId; use wayland_client::WEnum; use wayland_client::{protocol::wl_surface, Proxy}; @@ -49,19 +47,18 @@ struct RawWindow { display: *mut c_void, } -unsafe impl HasRawWindowHandle for RawWindow { - fn raw_window_handle(&self) -> RawWindowHandle { - let mut wh = blade_rwh::WaylandWindowHandle::empty(); - wh.surface = self.window; - wh.into() +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let window = NonNull::new(self.window).unwrap(); + let handle = rwh::WaylandWindowHandle::new(window); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) } } - -unsafe impl HasRawDisplayHandle for RawWindow { - fn raw_display_handle(&self) -> RawDisplayHandle { - let mut dh = blade_rwh::WaylandDisplayHandle::empty(); - dh.display = self.display; - dh.into() +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let display = NonNull::new(self.display).unwrap(); + let handle = rwh::WaylandDisplayHandle::new(display); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } } @@ -520,14 +517,13 @@ impl WaylandWindowStatePtr { } } -impl HasWindowHandle for WaylandWindow { - fn window_handle(&self) -> Result, HandleError> { +impl rwh::HasWindowHandle for WaylandWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { unimplemented!() } } - -impl HasDisplayHandle for WaylandWindow { - fn display_handle(&self) -> Result, HandleError> { +impl rwh::HasDisplayHandle for WaylandWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { unimplemented!() } } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 884011a376..a1d8532f71 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -77,8 +77,8 @@ pub struct Callbacks { } pub(crate) struct X11WindowState { - raw: RawWindow, atoms: XcbAtoms, + raw: RawWindow, bounds: Bounds, scale_factor: f32, renderer: BladeRenderer, @@ -96,40 +96,29 @@ pub(crate) struct X11Window { } // todo(linux): Remove other RawWindowHandle implementation -unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { - fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { - let mut wh = blade_rwh::XcbWindowHandle::empty(); - wh.window = self.window_id; - wh.visual_id = self.visual_id; - wh.into() +impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result { + let non_zero = NonZeroU32::new(self.window_id).unwrap(); + let handle = rwh::XcbWindowHandle::new(non_zero); + Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) } } -unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { - fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle { - let mut dh = blade_rwh::XcbDisplayHandle::empty(); - dh.connection = self.connection; - dh.screen = self.screen_id as i32; - dh.into() +impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result { + let non_zero = NonNull::new(self.connection).unwrap(); + let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } } impl rwh::HasWindowHandle for X11Window { fn window_handle(&self) -> Result { - Ok(unsafe { - let non_zero = NonZeroU32::new(self.state.borrow().raw.window_id).unwrap(); - let handle = rwh::XcbWindowHandle::new(non_zero); - rwh::WindowHandle::borrow_raw(handle.into()) - }) + unimplemented!() } } impl rwh::HasDisplayHandle for X11Window { fn display_handle(&self) -> Result { - Ok(unsafe { - let this = self.state.borrow(); - let non_zero = NonNull::new(this.raw.connection).unwrap(); - let handle = rwh::XcbDisplayHandle::new(Some(non_zero), this.raw.screen_id as i32); - rwh::DisplayHandle::borrow_raw(handle.into()) - }) + unimplemented!() } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index bcabaa786f..0780d89e7b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -31,10 +31,7 @@ use objc::{ sel, sel_impl, }; use parking_lot::Mutex; -use raw_window_handle::{ - AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HasDisplayHandle, HasWindowHandle, - RawWindowHandle, WindowHandle, -}; +use raw_window_handle as rwh; use smallvec::SmallVec; use std::{ any::Any, @@ -1141,25 +1138,25 @@ impl PlatformWindow for MacWindow { } } -impl HasWindowHandle for MacWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { +impl rwh::HasWindowHandle for MacWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { // SAFETY: The AppKitWindowHandle is a wrapper around a pointer to an NSView unsafe { - Ok(WindowHandle::borrow_raw(RawWindowHandle::AppKit( - AppKitWindowHandle::new(self.0.lock().native_view.cast()), + Ok(rwh::WindowHandle::borrow_raw(rwh::RawWindowHandle::AppKit( + rwh::AppKitWindowHandle::new(self.0.lock().native_view.cast()), ))) } } } -impl HasDisplayHandle for MacWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { +impl rwh::HasDisplayHandle for MacWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { // SAFETY: This is a no-op on macOS - unsafe { Ok(DisplayHandle::borrow_raw(AppKitDisplayHandle::new().into())) } + unsafe { + Ok(rwh::DisplayHandle::borrow_raw( + rwh::AppKitDisplayHandle::new().into(), + )) + } } } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 77b80eed2e..301dab271f 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -3,7 +3,6 @@ use std::{ any::Any, cell::{Cell, RefCell}, - ffi::c_void, iter::once, num::NonZeroIsize, path::PathBuf, @@ -18,7 +17,7 @@ use anyhow::Context; use blade_graphics as gpu; use futures::channel::oneshot::{self, Receiver}; use itertools::Itertools; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; +use raw_window_handle as rwh; use smallvec::SmallVec; use std::result::Result; use windows::{ @@ -77,20 +76,24 @@ impl WindowsWindowInner { let scale_factor = Cell::new(monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32); let input_handler = Cell::new(None); struct RawWindow { - hwnd: *mut c_void, + hwnd: isize, } - unsafe impl blade_rwh::HasRawWindowHandle for RawWindow { - fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle { - let mut handle = blade_rwh::Win32WindowHandle::empty(); - handle.hwnd = self.hwnd; - handle.into() + impl rwh::HasWindowHandle for RawWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + Ok(unsafe { + let hwnd = NonZeroIsize::new_unchecked(self.hwnd); + let handle = rwh::Win32WindowHandle::new(hwnd); + rwh::WindowHandle::borrow_raw(handle.into()) + }) } } - unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow { - fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle { - blade_rwh::WindowsDisplayHandle::empty().into() + impl rwh::HasDisplayHandle for RawWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { + let handle = rwh::WindowsDisplayHandle::new(); + Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } } + let raw = RawWindow { hwnd: hwnd.0 as _ }; let gpu = Arc::new( unsafe { @@ -1316,23 +1319,18 @@ impl WindowsWindow { } } -impl HasWindowHandle for WindowsWindow { - fn window_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { - let raw = raw_window_handle::Win32WindowHandle::new(unsafe { - NonZeroIsize::new_unchecked(self.inner.hwnd.0) - }) - .into(); - Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(raw) }) +impl rwh::HasWindowHandle for WindowsWindow { + fn window_handle(&self) -> Result, rwh::HandleError> { + let raw = + rwh::Win32WindowHandle::new(unsafe { NonZeroIsize::new_unchecked(self.inner.hwnd.0) }) + .into(); + Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) }) } } // todo(windows) -impl HasDisplayHandle for WindowsWindow { - fn display_handle( - &self, - ) -> Result, raw_window_handle::HandleError> { +impl rwh::HasDisplayHandle for WindowsWindow { + fn display_handle(&self) -> Result, rwh::HandleError> { unimplemented!() } } From b673494f4df8d24e72587614c67260dc6b74603d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 17:47:25 -0400 Subject: [PATCH 048/101] Restore the previous styles for single-line editors (#10951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes a bug introduced in #10870 that caused editors used as single-line inputs to have the wrong text style. If this change was intentional for something relating to the new Assistant panel, we'll need to figure out a way to change it without breaking these other usages. ### Before Screenshot 2024-04-24 at 5 35 36 PM Screenshot 2024-04-24 at 5 35 46 PM ### After Screenshot 2024-04-24 at 5 36 14 PM Screenshot 2024-04-24 at 5 36 31 PM Release Notes: - Fixed a bug where some inputs were using the wrong font style (preview-only). --- crates/editor/src/editor.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cfca895eff..a2fc4d67cf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10323,7 +10323,19 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let text_style = match self.mode { - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => cx.text_style(), + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(settings.buffer_line_height.value()), + background_color: None, + underline: None, + strikethrough: None, + white_space: WhiteSpace::Normal, + }, EditorMode::Full => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.buffer_font.family.clone(), From 64617a0ede83589b6d094395d402848fec3922ae Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 24 Apr 2024 16:06:36 -0600 Subject: [PATCH 049/101] Read settings in headless mode (#10950) Release Notes: - N/A --- crates/zed/src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ea5aafcb66..0ee87f7f41 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -138,6 +138,13 @@ fn init_headless(dev_server_token: DevServerToken) { languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); + let user_settings_file_rx = watch_config_file( + &cx.background_executor(), + fs.clone(), + paths::SETTINGS.clone(), + ); + handle_settings_file_changes(user_settings_file_rx, cx); + headless::init( client.clone(), headless::AppState { From 583a662ddc06f44dac2b8dc800423a5dab6f8d88 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 24 Apr 2024 18:56:36 -0400 Subject: [PATCH 050/101] Fix issues with drafting release notes in CI (#10955) This PR addresses some issues I ran into with the way we draft release notes in CI when doing builds. The first issue I encountered was that `script/draft-release-notes` was failing, seemingly due to CI doing a shallow Git checkout and not having all of the tags available in order to compare then. This was addressed by setting the `fetch-depth` during the Git checkout. The second issue is that (in my opinion) we shouldn't fail the build if drafting release notes fails. After well, we're doing it as a convenience to ourselves, and it isn't a mandatory part of the build. This was addressed by making any failures in `script/draft-release-notes` not fail the CI step as a whole. These changes were already applied to the `v0.133.x` branch. Release Notes: - N/A --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fa313d5ea..b81becdeff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,11 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 with: + # We need to fetch more than one commit so that `script/draft-release-notes` + # is able to diff between the current and previous tag. + # + # 25 was chosen arbitrarily. + fetch-depth: 25 clean: false submodules: "recursive" @@ -206,7 +211,8 @@ jobs: exit 1 fi mkdir -p target/ - script/draft-release-notes "$version" "$channel" > target/release-notes.md + # Ignore any errors that occur while drafting release notes to not fail the build. + script/draft-release-notes "$version" "$channel" > target/release-notes.md || true - name: Generate license file run: script/generate-licenses From d1425603f6e0fdcd6fe67c838b4c204e91109a29 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 24 Apr 2024 19:17:10 -0600 Subject: [PATCH 051/101] Fix misalignment of vim mode indicator (#10962) Credit-to: @elkowar New is the top Screenshot 2024-04-24 at 19 00 48 Release Notes: - N/A --- crates/vim/src/mode_indicator.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index a7033bc220..9c9f6f3207 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -55,6 +55,7 @@ impl Render for ModeIndicator { Label::new(format!("{} -- {} --", self.operators, mode)) .size(LabelSize::Small) + .line_height_style(LineHeightStyle::UiLabel) .into_any_element() } } From 1a27016123524dcd0e3d8c9f2c316f17223f4422 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 25 Apr 2024 11:19:15 +0800 Subject: [PATCH 052/101] Improve logic for obtaining surrounds range in Vim mode (#10938) now correctly retrieves range in cases where escape characters are present. Fixed #10827 Release Notes: - vim: Fix logic for finding surrounding quotes to ignore escaped characters (#10827) --- crates/vim/Cargo.toml | 1 + crates/vim/src/object.rs | 84 +++++++++++++++---- ...ounding_character_objects_with_escape.json | 10 +++ 3 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 5d6c1288b5..3efd05259e 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -23,6 +23,7 @@ collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true gpui.workspace = true +itertools.workspace = true language.workspace = true log.workspace = true nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = [ diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index cd08c052ed..a3cd89fbe3 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -9,6 +9,9 @@ use editor::{ movement::{self, FindRange}, Bias, DisplayPoint, }; + +use itertools::Itertools; + use gpui::{actions, impl_actions, ViewContext, WindowContext}; use language::{char_kind, BufferSnapshot, CharKind, Point, Selection}; use serde::Deserialize; @@ -801,15 +804,20 @@ fn surrounding_markers( let mut matched_closes = 0; let mut opening = None; + let mut before_ch = match movement::chars_before(map, point).next() { + Some((ch, _)) => ch, + _ => '\0', + }; if let Some((ch, range)) = movement::chars_after(map, point).next() { - if ch == open_marker { + if ch == open_marker && before_ch != '\\' { if open_marker == close_marker { let mut total = 0; - for (ch, _) in movement::chars_before(map, point) { + for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() + { if ch == '\n' { break; } - if ch == open_marker { + if ch == open_marker && before_ch != '\\' { total += 1; } } @@ -823,11 +831,15 @@ fn surrounding_markers( } if opening.is_none() { - for (ch, range) in movement::chars_before(map, point) { + for ((ch, range), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() { if ch == '\n' && !search_across_lines { break; } + if before_ch == '\\' { + continue; + } + if ch == open_marker { if matched_closes == 0 { opening = Some(range); @@ -839,15 +851,18 @@ fn surrounding_markers( } } } - if opening.is_none() { for (ch, range) in movement::chars_after(map, point) { - if ch == open_marker { - opening = Some(range); - break; - } else if ch == close_marker { - break; + if before_ch != '\\' { + if ch == open_marker { + opening = Some(range); + break; + } else if ch == close_marker { + break; + } } + + before_ch = ch; } } @@ -857,21 +872,28 @@ fn surrounding_markers( let mut matched_opens = 0; let mut closing = None; - + before_ch = match movement::chars_before(map, opening.end).next() { + Some((ch, _)) => ch, + _ => '\0', + }; for (ch, range) in movement::chars_after(map, opening.end) { if ch == '\n' && !search_across_lines { break; } - if ch == close_marker { - if matched_opens == 0 { - closing = Some(range); - break; + if before_ch != '\\' { + if ch == close_marker { + if matched_opens == 0 { + closing = Some(range); + break; + } + matched_opens -= 1; + } else if ch == open_marker { + matched_opens += 1; } - matched_opens -= 1; - } else if ch == open_marker { - matched_opens += 1; } + + before_ch = ch; } let Some(mut closing) = closing else { @@ -1467,6 +1489,32 @@ mod test { .await; } + #[gpui::test] + async fn test_singleline_surrounding_character_objects_with_escape( + cx: &mut gpui::TestAppContext, + ) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! { + "h\"e\\\"lˇlo \\\"world\"!" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "h\"«e\\\"llo \\\"worldˇ»\"!" + }) + .await; + + cx.set_shared_state(indoc! { + "hello \"teˇst \\\"inside\\\" world\"" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "\""]).await; + cx.assert_shared_state(indoc! { + "hello \"«test \\\"inside\\\" worldˇ»\"" + }) + .await; + } + #[gpui::test] async fn test_vertical_bars(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json b/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json new file mode 100644 index 0000000000..0de952ac91 --- /dev/null +++ b/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json @@ -0,0 +1,10 @@ +{"Put":{"state":"h\"e\\\"lˇlo \\\"world\"!"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"h\"«e\\\"llo \\\"worldˇ»\"!","mode":"Visual"}} +{"Put":{"state":"hello \"teˇst \\\"inside\\\" world\""}} +{"Key":"v"} +{"Key":"i"} +{"Key":"\""} +{"Get":{"state":"hello \"«test \\\"inside\\\" worldˇ»\"","mode":"Visual"}} From 031580f4dcb53ba165305645f886653dc576afc5 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 25 Apr 2024 11:29:29 +0200 Subject: [PATCH 053/101] git: Fix inline blame moving on horizontal scroll (#10974) This fixes the behaviour reported by @mikayla-maki. ## Before https://github.com/zed-industries/zed/assets/1185253/35aa4e6d-295b-4050-b5cc-cab0f91b27e1 ## After https://github.com/zed-industries/zed/assets/1185253/a17cbc9c-fc2c-43d6-918b-1205b327507b ## Release notes Release Notes: - Fixed inline git blame information moving when horizontally scrolling. --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b886c5db1f..1000952cf6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1200,7 +1200,7 @@ impl EditorElement { .map(|col| self.column_pixels(col as usize, cx)) .unwrap_or(px(0.)); - content_origin.x + max(padded_line_width, min_column) + (content_origin.x - scroll_pixel_position.x) + max(padded_line_width, min_column) }; let absolute_offset = point(start_x, start_y); From 6a7761e620cae20d91ace3b6c77e92d16f83f984 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 25 Apr 2024 12:54:39 +0200 Subject: [PATCH 054/101] Merge `ElementContext` into `WindowContext` (#10979) The new `ElementContext` was originally introduced to ensure the element APIs could only be used inside of elements. Unfortunately, there were many places where some of those APIs needed to be used, so `WindowContext::with_element_context` was introduced, which defeated the original safety purposes of having a specific context for elements. This pull request merges `ElementContext` into `WindowContext` and adds (debug) runtime checks to APIs that can only be used during certain phases of element drawing. Release Notes: - N/A --------- Co-authored-by: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 713 ++++---- crates/editor/src/blame_entry_tooltip.rs | 20 +- crates/editor/src/display_map/block_map.rs | 6 +- crates/editor/src/element.rs | 245 +-- crates/gpui/src/app/test_context.rs | 40 +- crates/gpui/src/element.rs | 84 +- crates/gpui/src/elements/anchored.rs | 10 +- crates/gpui/src/elements/animation.rs | 6 +- crates/gpui/src/elements/canvas.rs | 16 +- crates/gpui/src/elements/deferred.rs | 8 +- crates/gpui/src/elements/div.rs | 68 +- crates/gpui/src/elements/img.rs | 14 +- crates/gpui/src/elements/list.rs | 22 +- crates/gpui/src/elements/svg.rs | 12 +- crates/gpui/src/elements/text.rs | 35 +- crates/gpui/src/elements/uniform_list.rs | 14 +- crates/gpui/src/key_dispatch.rs | 9 +- crates/gpui/src/style.rs | 10 +- crates/gpui/src/text_system/line.rs | 10 +- crates/gpui/src/view.rs | 26 +- crates/gpui/src/window.rs | 1709 +++++++++++++++++- crates/gpui/src/window/element_cx.rs | 1554 ---------------- crates/image_viewer/src/image_viewer.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 24 +- crates/ui/src/components/popover_menu.rs | 16 +- crates/ui/src/components/right_click_menu.rs | 46 +- crates/ui/src/prelude.rs | 6 +- crates/workspace/src/pane_group.rs | 8 +- crates/workspace/src/workspace.rs | 12 +- 29 files changed, 2378 insertions(+), 2367 deletions(-) delete mode 100644 crates/gpui/src/window/element_cx.rs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index fc7f8de8a0..685c37ac60 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -912,7 +912,7 @@ mod tests { display_map::{BlockContext, TransformBlock}, DisplayPoint, GutterDimensions, }; - use gpui::{px, Stateful, TestAppContext, VisualTestContext, WindowContext}; + use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use project::FakeFs; use serde_json::json; @@ -1049,67 +1049,66 @@ mod tests { cx, ) }); + let editor = view.update(cx, |view, _| view.editor.clone()); view.next_notification(cx).await; - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (15, "collapsed context".into()), - (16, "diagnostic header".into()), - (25, "collapsed context".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (15, "collapsed context".into()), + (16, "diagnostic header".into()), + (25, "collapsed context".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); - // Cursor is at the first diagnostic - view.editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.display_ranges(cx), - [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)] - ); - }); + // Cursor is at the first diagnostic + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)] + ); }); // Diagnostics are added for another earlier path. @@ -1138,78 +1137,77 @@ mod tests { }); view.next_notification(cx).await; - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "path header block".into()), - (9, "diagnostic header".into()), - (22, "collapsed context".into()), - (23, "diagnostic header".into()), - (32, "collapsed context".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "path header block".into()), + (9, "diagnostic header".into()), + (22, "collapsed context".into()), + (23, "diagnostic header".into()), + (32, "collapsed context".into()), + ] + ); - // Cursor keeps its position. - view.editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.display_ranges(cx), - [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)] - ); - }); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + + // Cursor keeps its position. + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)] + ); }); // Diagnostics are added to the first path @@ -1254,80 +1252,79 @@ mod tests { }); view.next_notification(cx).await; - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - (13, "path header block".into()), - (15, "diagnostic header".into()), - (28, "collapsed context".into()), - (29, "diagnostic header".into()), - (38, "collapsed context".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "const b: i32 = c;\n", - "\n", // supporting diagnostic - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); - }); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + (13, "path header block".into()), + (15, "diagnostic header".into()), + (28, "collapsed context".into()), + (29, "diagnostic header".into()), + (38, "collapsed context".into()), + ] + ); + + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "const b: i32 = c;\n", + "\n", // supporting diagnostic + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); } #[gpui::test] @@ -1364,6 +1361,7 @@ mod tests { cx, ) }); + let editor = view.update(cx, |view, _| view.editor.clone()); // Two language servers start updating diagnostics project.update(cx, |project, cx| { @@ -1397,27 +1395,25 @@ mod tests { // Only the first language server's diagnostics are shown. cx.executor().run_until_parked(); - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // - "b();", - ) - ); - }); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // + "b();", + ) + ); // The second language server finishes project.update(cx, |project, cx| { @@ -1445,36 +1441,34 @@ mod tests { // Both language server's diagnostics are shown. cx.executor().run_until_parked(); - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (6, "collapsed context".into()), - (7, "diagnostic header".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "a();\n", // context - "b();\n", // - "c();", // context - ) - ); - }); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (6, "collapsed context".into()), + (7, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "a();\n", // context + "b();\n", // + "c();", // context + ) + ); // Both language servers start updating diagnostics, and the first server finishes. project.update(cx, |project, cx| { @@ -1513,37 +1507,35 @@ mod tests { // Only the first language server's diagnostics are updated. cx.executor().run_until_parked(); - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "c();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "b();\n", // context - "c();\n", // - "d();", // context - ) - ); - }); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "c();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "b();\n", // context + "c();\n", // + "d();", // context + ) + ); // The second language server finishes. project.update(cx, |project, cx| { @@ -1571,37 +1563,35 @@ mod tests { // Both language servers' diagnostics are updated. cx.executor().run_until_parked(); - view.update(cx, |view, cx| { - assert_eq!( - editor_blocks(&view.editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - ] - ); - assert_eq!( - view.editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "b();\n", // location - "c();\n", // - "d();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "c();\n", // context - "d();\n", // - "e();", // context - ) - ); - }); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "b();\n", // location + "c();\n", // + "d();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "c();\n", // context + "d();\n", // + "e();", // context + ) + ); } fn init_test(cx: &mut TestAppContext) { @@ -1618,45 +1608,58 @@ mod tests { }); } - fn editor_blocks(editor: &View, cx: &mut WindowContext) -> Vec<(u32, SharedString)> { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - snapshot - .blocks_in_range(0..snapshot.max_point().row()) - .enumerate() - .filter_map(|(ix, (row, block))| { - let name: SharedString = match block { - TransformBlock::Custom(block) => cx.with_element_context({ - |cx| -> Option { - let mut element = block.render(&mut BlockContext { - context: cx, - anchor_x: px(0.), - gutter_dimensions: &GutterDimensions::default(), - line_height: px(0.), - em_width: px(0.), - max_width: px(0.), - block_id: ix, - editor_style: &editor::EditorStyle::default(), - }); - let element = element.downcast_mut::>().unwrap(); - element.interactivity().element_id.clone()?.try_into().ok() - } - })?, + fn editor_blocks( + editor: &View, + cx: &mut VisualTestContext, + ) -> Vec<(u32, SharedString)> { + let mut blocks = Vec::new(); + cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + blocks.extend( + snapshot + .blocks_in_range(0..snapshot.max_point().row()) + .enumerate() + .filter_map(|(ix, (row, block))| { + let name: SharedString = match block { + TransformBlock::Custom(block) => { + let mut element = block.render(&mut BlockContext { + context: cx, + anchor_x: px(0.), + gutter_dimensions: &GutterDimensions::default(), + line_height: px(0.), + em_width: px(0.), + max_width: px(0.), + block_id: ix, + editor_style: &editor::EditorStyle::default(), + }); + let element = element.downcast_mut::>().unwrap(); + element + .interactivity() + .element_id + .clone()? + .try_into() + .ok()? + } - TransformBlock::ExcerptHeader { - starts_new_buffer, .. - } => { - if *starts_new_buffer { - "path header block".into() - } else { - "collapsed context".into() - } - } - }; + TransformBlock::ExcerptHeader { + starts_new_buffer, .. + } => { + if *starts_new_buffer { + "path header block".into() + } else { + "collapsed context".into() + } + } + }; - Some((row, name)) - }) - .collect() - }) + Some((row, name)) + }), + ) + }); + + div().into_any() + }); + blocks } } diff --git a/crates/editor/src/blame_entry_tooltip.rs b/crates/editor/src/blame_entry_tooltip.rs index ac8c6bfc05..3732cf4caf 100644 --- a/crates/editor/src/blame_entry_tooltip.rs +++ b/crates/editor/src/blame_entry_tooltip.rs @@ -39,17 +39,15 @@ impl<'a> CommitAvatar<'a> { let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha); - let element = cx.with_element_context(|cx| { - match cx.use_cached_asset::(&avatar_url) { - // Loading or no avatar found - None | Some(None) => Icon::new(IconName::Person) - .color(Color::Muted) - .into_element() - .into_any(), - // Found - Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(), - } - }); + let element = match cx.use_cached_asset::(&avatar_url) { + // Loading or no avatar found + None | Some(None) => Icon::new(IconName::Person) + .color(Color::Muted) + .into_element() + .into_any(), + // Found + Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(), + }; Some(element) } } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 6de21b0d08..026d5365cf 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{EditorStyle, GutterDimensions}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, ElementContext, Pixels}; +use gpui::{AnyElement, Pixels, WindowContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ToPoint as _}; use parking_lot::Mutex; @@ -82,7 +82,7 @@ pub enum BlockStyle { } pub struct BlockContext<'a, 'b> { - pub context: &'b mut ElementContext<'a>, + pub context: &'b mut WindowContext<'a>, pub anchor_x: Pixels, pub max_width: Pixels, pub gutter_dimensions: &'b GutterDimensions, @@ -934,7 +934,7 @@ impl BlockDisposition { } impl<'a> Deref for BlockContext<'a, '_> { - type Target = ElementContext<'a>; + type Target = WindowContext<'a>; fn deref(&self) -> &Self::Target { self.context diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1000952cf6..42b6bfb90b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -23,12 +23,11 @@ use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; 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, ElementContext, - ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful, - StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, - ViewContext, WeakView, WindowContext, + ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hitbox, + Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, + ShapedLine, SharedString, Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, + TextStyle, TextStyleRefinement, View, ViewContext, WeakView, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -367,7 +366,7 @@ impl EditorElement { register_action(view, cx, Editor::open_active_item_in_terminal) } - fn register_key_listeners(&self, cx: &mut ElementContext, layout: &EditorLayout) { + fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) { let position_map = layout.position_map.clone(); cx.on_key_event({ let editor = self.editor.clone(); @@ -691,7 +690,7 @@ impl EditorElement { snapshot: &EditorSnapshot, start_row: u32, end_row: u32, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> ( Vec<(PlayerColor, Vec)>, BTreeMap, @@ -819,7 +818,7 @@ impl EditorElement { scroll_pixel_position: gpui::Point, line_height: Pixels, line_layouts: &[LineWithInvisibles], - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Vec { snapshot .folds_in_range(visible_anchor_range.clone()) @@ -887,7 +886,7 @@ impl EditorElement { line_height: Pixels, em_width: Pixels, autoscroll_containing_element: bool, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Vec { let mut autoscroll_bounds = None; let cursor_layouts = self.editor.update(cx, |editor, cx| { @@ -993,7 +992,7 @@ impl EditorElement { color: self.style.background, is_top_row: cursor_position.row() == 0, }); - cx.with_element_context(|cx| cursor.layout(content_origin, cursor_name, cx)); + cursor.layout(content_origin, cursor_name, cx); cursors.push(cursor); } } @@ -1013,7 +1012,7 @@ impl EditorElement { bounds: Bounds, scroll_position: gpui::Point, rows_per_page: f32, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; let show_scrollbars = match scrollbar_settings.show { @@ -1082,7 +1081,7 @@ impl EditorElement { gutter_settings: crate::editor_settings::Gutter, scroll_pixel_position: gpui::Point, gutter_hitbox: &Hitbox, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Vec> { let mut indicators = self.editor.update(cx, |editor, cx| { editor.render_fold_indicators( @@ -1155,7 +1154,7 @@ impl EditorElement { content_origin: gpui::Point, scroll_pixel_position: gpui::Point, line_height: Pixels, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { if !self .editor @@ -1220,7 +1219,7 @@ impl EditorElement { line_height: Pixels, gutter_hitbox: &Hitbox, max_width: Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option> { if !self .editor @@ -1285,7 +1284,7 @@ impl EditorElement { scroll_pixel_position: gpui::Point, gutter_dimensions: &GutterDimensions, gutter_hitbox: &Hitbox, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { let mut active = false; let mut button = None; @@ -1372,7 +1371,7 @@ impl EditorElement { active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, - cx: &ElementContext, + cx: &WindowContext, ) -> ( Vec>, Vec>, @@ -1465,7 +1464,7 @@ impl EditorElement { rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, - cx: &ElementContext, + cx: &WindowContext, ) -> Vec { if rows.start >= rows.end { return Vec::new(); @@ -1530,7 +1529,7 @@ impl EditorElement { text_x: Pixels, line_height: Pixels, line_layouts: &[LineWithInvisibles], - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Vec { let mut block_id = 0; let (fixed_blocks, non_fixed_blocks) = snapshot @@ -1544,7 +1543,7 @@ impl EditorElement { available_space: Size, block_id: usize, block_row_start: u32, - cx: &mut ElementContext| { + cx: &mut WindowContext| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -1865,7 +1864,7 @@ impl EditorElement { hitbox: &Hitbox, line_height: Pixels, scroll_pixel_position: gpui::Point, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { for block in blocks { let mut origin = hitbox.origin @@ -1893,7 +1892,7 @@ impl EditorElement { scroll_pixel_position: gpui::Point, line_layouts: &[LineWithInvisibles], newest_selection_head: DisplayPoint, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> bool { let max_height = cmp::min( 12. * line_height, @@ -1933,7 +1932,7 @@ impl EditorElement { true } - fn layout_mouse_context_menu(&self, cx: &mut ElementContext) -> Option { + fn layout_mouse_context_menu(&self, cx: &mut WindowContext) -> Option { let mouse_context_menu = self.editor.read(cx).mouse_context_menu.as_ref()?; let mut element = deferred( anchored() @@ -1961,7 +1960,7 @@ impl EditorElement { line_layouts: &[LineWithInvisibles], line_height: Pixels, em_width: Pixels, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { struct MeasuredHoverPopover { element: AnyElement, @@ -2021,7 +2020,7 @@ impl EditorElement { } overall_height += HOVER_POPOVER_GAP; - fn draw_occluder(width: Pixels, origin: gpui::Point, cx: &mut ElementContext) { + fn draw_occluder(width: Pixels, origin: gpui::Point, cx: &mut WindowContext) { let mut occlusion = div() .size_full() .occlude() @@ -2067,7 +2066,7 @@ impl EditorElement { } } - fn paint_background(&self, layout: &EditorLayout, cx: &mut ElementContext) { + fn paint_background(&self, layout: &EditorLayout, cx: &mut WindowContext) { cx.paint_layer(layout.hitbox.bounds, |cx| { let scroll_top = layout.position_map.snapshot.scroll_position().y; let gutter_bg = cx.theme().colors().editor_gutter_background; @@ -2188,7 +2187,7 @@ impl EditorElement { }) } - fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { let line_height = layout.position_map.line_height; let scroll_position = layout.position_map.snapshot.scroll_position(); @@ -2236,7 +2235,7 @@ impl EditorElement { }) } - fn paint_diff_hunks(layout: &EditorLayout, cx: &mut ElementContext) { + fn paint_diff_hunks(layout: &EditorLayout, cx: &mut WindowContext) { if layout.display_hunks.is_empty() { return; } @@ -2342,7 +2341,7 @@ impl EditorElement { }) } - fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) { let Some(blamed_display_rows) = layout.blamed_display_rows.take() else { return; }; @@ -2354,7 +2353,7 @@ impl EditorElement { }) } - fn paint_text(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_text(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { cx.with_content_mask( Some(ContentMask { bounds: layout.text_hitbox.bounds, @@ -2386,7 +2385,7 @@ impl EditorElement { fn paint_highlights( &mut self, layout: &mut EditorLayout, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> SmallVec<[Range; 32]> { cx.paint_layer(layout.text_hitbox.bounds, |cx| { let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); @@ -2428,7 +2427,7 @@ impl EditorElement { &mut self, invisible_display_ranges: &[Range], layout: &EditorLayout, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let whitespace_setting = self .editor @@ -2451,7 +2450,7 @@ impl EditorElement { } } - fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { + fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { if layout.redacted_ranges.is_empty() { return; } @@ -2475,13 +2474,13 @@ impl EditorElement { }); } - fn paint_cursors(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_cursors(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { for cursor in &mut layout.cursors { cursor.paint(layout.content_origin, cx); } } - fn paint_scrollbar(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_scrollbar(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { let Some(scrollbar_layout) = layout.scrollbar_layout.as_ref() else { return; }; @@ -2617,7 +2616,7 @@ impl EditorElement { &self, layout: &EditorLayout, scrollbar_layout: &ScrollbarLayout, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.editor.update(cx, |editor, cx| { if !editor.is_singleton(cx) @@ -2775,7 +2774,7 @@ impl EditorElement { corner_radius: Pixels, line_end_overshoot: Pixels, layout: &EditorLayout, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let start_row = layout.visible_display_row_range.start; let end_row = layout.visible_display_row_range.end; @@ -2824,7 +2823,7 @@ impl EditorElement { } } - fn paint_folds(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_folds(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { if layout.folds.is_empty() { return; } @@ -2855,7 +2854,7 @@ impl EditorElement { }) } - fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + 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| { inline_blame.paint(cx); @@ -2863,19 +2862,19 @@ impl EditorElement { } } - fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { for mut block in layout.blocks.drain(..) { block.element.paint(cx); } } - fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) { + 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); } } - fn paint_scroll_wheel_listener(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { + fn paint_scroll_wheel_listener(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); @@ -2925,7 +2924,7 @@ impl EditorElement { }); } - fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut ElementContext) { + fn paint_mouse_listeners(&mut self, layout: &EditorLayout, cx: &mut WindowContext) { self.paint_scroll_wheel_listener(layout, cx); cx.on_mouse_event({ @@ -3042,7 +3041,7 @@ fn render_inline_blame_entry( blame_entry: BlameEntry, style: &EditorStyle, workspace: Option>, - cx: &mut ElementContext<'_>, + cx: &mut WindowContext<'_>, ) -> AnyElement { let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx); @@ -3073,7 +3072,7 @@ fn render_blame_entry( style: &EditorStyle, last_used_color: &mut Option<(PlayerColor, Oid)>, editor: View, - cx: &mut ElementContext<'_>, + cx: &mut WindowContext<'_>, ) -> AnyElement { let mut sha_color = cx .theme() @@ -3286,7 +3285,7 @@ impl LineWithInvisibles { content_origin: gpui::Point, whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let line_height = layout.position_map.line_height; let line_y = @@ -3318,7 +3317,7 @@ impl LineWithInvisibles { row: u32, line_height: Pixels, whitespace_setting: ShowWhitespaceSetting, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let allowed_invisibles_regions = match whitespace_setting { ShowWhitespaceSetting::None => return, @@ -3365,7 +3364,7 @@ impl Element for EditorElement { type RequestLayoutState = (); type PrepaintState = EditorLayout; - fn request_layout(&mut self, cx: &mut ElementContext) -> (gpui::LayoutId, ()) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (gpui::LayoutId, ()) { self.editor.update(cx, |editor, cx| { editor.set_style(self.style.clone(), cx); @@ -3375,36 +3374,31 @@ impl Element for EditorElement { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); - cx.with_element_context(|cx| cx.request_layout(&style, None)) + cx.request_layout(&style, None) } EditorMode::AutoHeight { max_lines } => { let editor_handle = cx.view().clone(); let max_line_number_width = self.max_line_number_width(&editor.snapshot(cx), cx); - cx.with_element_context(|cx| { - cx.request_measured_layout( - Style::default(), - move |known_dimensions, _, cx| { - editor_handle - .update(cx, |editor, cx| { - compute_auto_height_layout( - editor, - max_lines, - max_line_number_width, - known_dimensions, - cx, - ) - }) - .unwrap_or_default() - }, - ) + cx.request_measured_layout(Style::default(), move |known_dimensions, _, cx| { + editor_handle + .update(cx, |editor, cx| { + compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + ) + }) + .unwrap_or_default() }) } EditorMode::Full => { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); - cx.with_element_context(|cx| cx.request_layout(&style, None)) + cx.request_layout(&style, None) } }; @@ -3416,7 +3410,7 @@ impl Element for EditorElement { &mut self, bounds: Bounds, _: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Self::PrepaintState { let text_style = TextStyleRefinement { font_size: Some(self.style.text.font_size), @@ -3847,13 +3841,12 @@ impl Element for EditorElement { bounds: Bounds, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let focus_handle = self.editor.focus_handle(cx); let key_context = self.editor.read(cx).key_context(cx); cx.set_focus_handle(&focus_handle); cx.set_key_context(key_context); - cx.set_view_id(self.editor.entity_id()); cx.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.editor.clone()), @@ -4206,7 +4199,7 @@ impl CursorLayout { &mut self, origin: gpui::Point, cursor_name: Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { if let Some(cursor_name) = cursor_name { let bounds = self.bounds(origin); @@ -4236,7 +4229,7 @@ impl CursorLayout { } } - pub fn paint(&mut self, origin: gpui::Point, cx: &mut ElementContext) { + pub fn paint(&mut self, origin: gpui::Point, cx: &mut WindowContext) { let bounds = self.bounds(origin); //Draw background or border quad @@ -4280,7 +4273,7 @@ pub struct HighlightedRangeLine { } impl HighlightedRange { - pub fn paint(&self, bounds: Bounds, cx: &mut ElementContext) { + pub fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx); self.paint_lines( @@ -4299,7 +4292,7 @@ impl HighlightedRange { start_y: Pixels, lines: &[HighlightedRangeLine], _bounds: Bounds, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { if lines.is_empty() { return; @@ -4416,7 +4409,7 @@ mod tests { editor_tests::{init_test, update_test_language_settings}, Editor, MultiBuffer, }; - use gpui::TestAppContext; + use gpui::{TestAppContext, VisualTestContext}; use language::language_settings; use log::info; use std::num::NonZeroU32; @@ -4437,18 +4430,16 @@ mod tests { let layouts = cx .update_window(*window, |_, cx| { - cx.with_element_context(|cx| { - element - .layout_line_numbers( - 0..6, - (0..6).map(Some), - &Default::default(), - Some(DisplayPoint::new(0, 0)), - &snapshot, - cx, - ) - .0 - }) + element + .layout_line_numbers( + 0..6, + (0..6).map(Some), + &Default::default(), + Some(DisplayPoint::new(0, 0)), + &snapshot, + cx, + ) + .0 }) .unwrap(); assert_eq!(layouts.len(), 6); @@ -4487,9 +4478,9 @@ mod tests { let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); Editor::new(EditorMode::Full, buffer, None, cx) }); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); window .update(cx, |editor, cx| { @@ -4503,20 +4494,10 @@ mod tests { }); }) .unwrap(); - let state = cx - .update_window(window.into(), |_view, cx| { - cx.with_element_context(|cx| { - element.prepaint( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - &mut (), - cx, - ) - }) - }) - .unwrap(); + + let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| { + EditorElement::new(&editor, style) + }); assert_eq!(state.selections.len(), 1); let local_selections = &state.selections[0].1; @@ -4587,7 +4568,6 @@ mod tests { }); let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); let _state = window.update(cx, |editor, cx| { editor.cursor_shape = CursorShape::Block; editor.change_selections(None, cx, |s| { @@ -4598,20 +4578,9 @@ mod tests { }); }); - let state = cx - .update_window(window.into(), |_view, cx| { - cx.with_element_context(|cx| { - element.prepaint( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - &mut (), - cx, - ) - }) - }) - .unwrap(); + let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| { + EditorElement::new(&editor, style) + }); assert_eq!(state.selections.len(), 1); let local_selections = &state.selections[0].1; assert_eq!(local_selections.len(), 2); @@ -4640,6 +4609,7 @@ mod tests { let buffer = MultiBuffer::build_simple("", cx); Editor::new(EditorMode::Full, buffer, None, cx) }); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); window @@ -4662,22 +4632,9 @@ mod tests { }) .unwrap(); - let mut element = EditorElement::new(&editor, style); - let state = cx - .update_window(window.into(), |_view, cx| { - cx.with_element_context(|cx| { - element.prepaint( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - &mut (), - cx, - ) - }) - }) - .unwrap(); - + let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| { + EditorElement::new(&editor, style) + }); assert_eq!(state.position_map.line_layouts.len(), 4); assert_eq!( state @@ -4850,31 +4807,19 @@ mod tests { let buffer = MultiBuffer::build_simple(&input_text, cx); Editor::new(editor_mode, buffer, None, cx) }); + let cx = &mut VisualTestContext::from_window(*window, cx); let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); - let mut element = EditorElement::new(&editor, style); window .update(cx, |editor, cx| { editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); editor.set_wrap_width(Some(editor_width), cx); }) .unwrap(); - let layout_state = cx - .update_window(window.into(), |_, cx| { - cx.with_element_context(|cx| { - element.prepaint( - Bounds { - origin: point(px(500.), px(500.)), - size: size(px(500.), px(500.)), - }, - &mut (), - cx, - ) - }) - }) - .unwrap(); - - layout_state + let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| { + EditorElement::new(&editor, style) + }); + state .position_map .line_layouts .iter() diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 7a6f3b9e27..a17f8defe9 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,10 +1,11 @@ use crate::{ - Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - AvailableSpace, BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, 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, WindowContext, WindowHandle, WindowOptions, + Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace, + BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable, + 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, WindowContext, + WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{channel::oneshot, Stream, StreamExt}; @@ -725,21 +726,28 @@ impl VisualTestContext { } /// Draw an element to the window. Useful for simulating events or actions - pub fn draw( + pub fn draw( &mut self, origin: Point, - space: Size, - f: impl FnOnce(&mut WindowContext) -> AnyElement, - ) { + space: impl Into>, + f: impl FnOnce(&mut WindowContext) -> E, + ) -> (E::RequestLayoutState, E::PrepaintState) + where + E: Element, + { self.update(|cx| { - cx.with_element_context(|cx| { - let mut element = f(cx); - element.layout_as_root(space, cx); - cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx)); - element.paint(cx); - }); + cx.window.draw_phase = DrawPhase::Prepaint; + let mut element = Drawable::new(f(cx)); + element.layout_as_root(space.into(), cx); + cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx)); + cx.window.draw_phase = DrawPhase::Paint; + let (request_layout_state, prepaint_state) = element.paint(cx); + + cx.window.draw_phase = DrawPhase::None; cx.refresh(); + + (request_layout_state, prepaint_state) }) } diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index ddb728e437..c4079214dc 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -32,12 +32,12 @@ //! your own custom layout algorithm or rendering a code editor. use crate::{ - util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementContext, - ElementId, LayoutId, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA, + util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, LayoutId, + Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, mem, ops::DerefMut}; +use std::{any::Any, fmt::Debug, mem}; /// Implemented by types that participate in laying out and painting the contents of a window. /// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy. @@ -54,7 +54,7 @@ pub trait Element: 'static + IntoElement { /// Before an element can be painted, we need to know where it's going to be and how big it is. /// Use this method to request a layout from Taffy and initialize the element's state. - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState); + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState); /// After laying out an element, we need to commit its bounds to the current frame for hitbox /// purposes. The state argument is the same state that was returned from [`Element::request_layout()`]. @@ -62,7 +62,7 @@ pub trait Element: 'static + IntoElement { &mut self, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Self::PrepaintState; /// Once layout has been completed, this method will be called to paint the element to the screen. @@ -72,7 +72,7 @@ pub trait Element: 'static + IntoElement { bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ); /// Convert this element into a dynamically-typed [`AnyElement`]. @@ -164,18 +164,13 @@ impl Element for Component { type RequestLayoutState = AnyElement; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { - let mut element = self - .0 - .take() - .unwrap() - .render(cx.deref_mut()) - .into_any_element(); + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { + let mut element = self.0.take().unwrap().render(cx).into_any_element(); let layout_id = element.request_layout(cx); (layout_id, element) } - fn prepaint(&mut self, _: Bounds, element: &mut AnyElement, cx: &mut ElementContext) { + fn prepaint(&mut self, _: Bounds, element: &mut AnyElement, cx: &mut WindowContext) { element.prepaint(cx); } @@ -184,7 +179,7 @@ impl Element for Component { _: Bounds, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { element.paint(cx) } @@ -205,16 +200,16 @@ pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>); trait ElementObject { fn inner_element(&mut self) -> &mut dyn Any; - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId; + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId; - fn prepaint(&mut self, cx: &mut ElementContext); + fn prepaint(&mut self, cx: &mut WindowContext); - fn paint(&mut self, cx: &mut ElementContext); + fn paint(&mut self, cx: &mut WindowContext); fn layout_as_root( &mut self, available_space: Size, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Size; } @@ -249,14 +244,14 @@ enum ElementDrawPhase { /// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. impl Drawable { - fn new(element: E) -> Self { + pub(crate) fn new(element: E) -> Self { Drawable { element, phase: ElementDrawPhase::Start, } } - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { match mem::take(&mut self.phase) { ElementDrawPhase::Start => { let (layout_id, request_layout) = self.element.request_layout(cx); @@ -270,7 +265,7 @@ impl Drawable { } } - fn prepaint(&mut self, cx: &mut ElementContext) { + pub(crate) fn prepaint(&mut self, cx: &mut WindowContext) { match mem::take(&mut self.phase) { ElementDrawPhase::RequestLayoutState { layout_id, @@ -296,7 +291,10 @@ impl Drawable { } } - fn paint(&mut self, cx: &mut ElementContext) -> E::RequestLayoutState { + pub(crate) fn paint( + &mut self, + cx: &mut WindowContext, + ) -> (E::RequestLayoutState, E::PrepaintState) { match mem::take(&mut self.phase) { ElementDrawPhase::PrepaintState { node_id, @@ -309,16 +307,16 @@ impl Drawable { self.element .paint(bounds, &mut request_layout, &mut prepaint, cx); self.phase = ElementDrawPhase::Painted; - request_layout + (request_layout, prepaint) } _ => panic!("must call prepaint before paint"), } } - fn layout_as_root( + pub(crate) fn layout_as_root( &mut self, available_space: Size, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Size { if matches!(&self.phase, ElementDrawPhase::Start) { self.request_layout(cx); @@ -368,22 +366,22 @@ where &mut self.element } - fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { Drawable::request_layout(self, cx) } - fn prepaint(&mut self, cx: &mut ElementContext) { + fn prepaint(&mut self, cx: &mut WindowContext) { Drawable::prepaint(self, cx); } - fn paint(&mut self, cx: &mut ElementContext) { + fn paint(&mut self, cx: &mut WindowContext) { Drawable::paint(self, cx); } fn layout_as_root( &mut self, available_space: Size, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Size { Drawable::layout_as_root(self, available_space, cx) } @@ -411,18 +409,18 @@ impl AnyElement { /// Request the layout ID of the element stored in this `AnyElement`. /// Used for laying out child elements in a parent element. - pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId { + pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId { self.0.request_layout(cx) } /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and /// request autoscroll before the final paint pass is confirmed. - pub fn prepaint(&mut self, cx: &mut ElementContext) { + pub fn prepaint(&mut self, cx: &mut WindowContext) { self.0.prepaint(cx) } /// Paints the element stored in this `AnyElement`. - pub fn paint(&mut self, cx: &mut ElementContext) { + pub fn paint(&mut self, cx: &mut WindowContext) { self.0.paint(cx) } @@ -430,13 +428,13 @@ impl AnyElement { pub fn layout_as_root( &mut self, available_space: Size, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Size { self.0.layout_as_root(available_space, cx) } /// Prepaints this element at the given absolute origin. - pub fn prepaint_at(&mut self, origin: Point, cx: &mut ElementContext) { + pub fn prepaint_at(&mut self, origin: Point, cx: &mut WindowContext) { cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); } @@ -445,7 +443,7 @@ impl AnyElement { &mut self, origin: Point, available_space: Size, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.layout_as_root(available_space, cx); cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); @@ -456,7 +454,7 @@ impl Element for AnyElement { type RequestLayoutState = (); type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let layout_id = self.request_layout(cx); (layout_id, ()) } @@ -465,7 +463,7 @@ impl Element for AnyElement { &mut self, _: Bounds, _: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.prepaint(cx) } @@ -475,7 +473,7 @@ impl Element for AnyElement { _: Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.paint(cx) } @@ -508,15 +506,15 @@ impl Element for Empty { type RequestLayoutState = (); type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { - (cx.request_layout(&crate::Style::default(), None), ()) + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { + (cx.request_layout(&Style::default(), None), ()) } fn prepaint( &mut self, _bounds: Bounds, _state: &mut Self::RequestLayoutState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } @@ -525,7 +523,7 @@ impl Element for Empty { _bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } } diff --git a/crates/gpui/src/elements/anchored.rs b/crates/gpui/src/elements/anchored.rs index 9f4d342716..15421f4ab3 100644 --- a/crates/gpui/src/elements/anchored.rs +++ b/crates/gpui/src/elements/anchored.rs @@ -2,8 +2,8 @@ use smallvec::SmallVec; use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, ParentElement, - Pixels, Point, Size, Style, + point, AnyElement, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, Point, Size, + Style, WindowContext, }; /// The state that the anchored element element uses to track its children. @@ -74,7 +74,7 @@ impl Element for Anchored { fn request_layout( &mut self, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> (crate::LayoutId, Self::RequestLayoutState) { let child_layout_ids = self .children @@ -97,7 +97,7 @@ impl Element for Anchored { &mut self, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { if request_layout.child_layout_ids.is_empty() { return; @@ -180,7 +180,7 @@ impl Element for Anchored { _bounds: crate::Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { for child in &mut self.children { child.paint(cx); diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 586b26f7e9..f18ff3fcb8 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -91,7 +91,7 @@ impl Element for AnimationElement { fn request_layout( &mut self, - cx: &mut crate::ElementContext, + cx: &mut crate::WindowContext, ) -> (crate::LayoutId, Self::RequestLayoutState) { cx.with_element_state(Some(self.id.clone()), |state, cx| { let state = state.unwrap().unwrap_or_else(|| AnimationState { @@ -138,7 +138,7 @@ impl Element for AnimationElement { &mut self, _bounds: crate::Bounds, element: &mut Self::RequestLayoutState, - cx: &mut crate::ElementContext, + cx: &mut crate::WindowContext, ) -> Self::PrepaintState { element.prepaint(cx); } @@ -148,7 +148,7 @@ impl Element for AnimationElement { _bounds: crate::Bounds, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut crate::ElementContext, + cx: &mut crate::WindowContext, ) { element.paint(cx); } diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index c0bfc044ab..989ea76da5 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -1,12 +1,12 @@ use refineable::Refineable as _; -use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRefinement, Styled}; +use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; /// Construct a canvas element with the given paint callback. /// Useful for adding short term custom drawing to a view. pub fn canvas( - prepaint: impl 'static + FnOnce(Bounds, &mut ElementContext) -> T, - paint: impl 'static + FnOnce(Bounds, T, &mut ElementContext), + prepaint: impl 'static + FnOnce(Bounds, &mut WindowContext) -> T, + paint: impl 'static + FnOnce(Bounds, T, &mut WindowContext), ) -> Canvas { Canvas { prepaint: Some(Box::new(prepaint)), @@ -18,8 +18,8 @@ pub fn canvas( /// A canvas element, meant for accessing the low level paint API without defining a whole /// custom element pub struct Canvas { - prepaint: Option, &mut ElementContext) -> T>>, - paint: Option, T, &mut ElementContext)>>, + prepaint: Option, &mut WindowContext) -> T>>, + paint: Option, T, &mut WindowContext)>>, style: StyleRefinement, } @@ -37,7 +37,7 @@ impl Element for Canvas { fn request_layout( &mut self, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> (crate::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.refine(&self.style); @@ -49,7 +49,7 @@ impl Element for Canvas { &mut self, bounds: Bounds, _request_layout: &mut Style, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { Some(self.prepaint.take().unwrap()(bounds, cx)) } @@ -59,7 +59,7 @@ impl Element for Canvas { bounds: Bounds, style: &mut Style, prepaint: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let prepaint = prepaint.take().unwrap(); style.paint(bounds, cx, |cx| { diff --git a/crates/gpui/src/elements/deferred.rs b/crates/gpui/src/elements/deferred.rs index 30643bdc2a..9bf365ae0d 100644 --- a/crates/gpui/src/elements/deferred.rs +++ b/crates/gpui/src/elements/deferred.rs @@ -1,4 +1,4 @@ -use crate::{AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, Pixels}; +use crate::{AnyElement, Bounds, Element, IntoElement, LayoutId, Pixels, WindowContext}; /// Builds a `Deferred` element, which delays the layout and paint of its child. pub fn deferred(child: impl IntoElement) -> Deferred { @@ -29,7 +29,7 @@ impl Element for Deferred { type RequestLayoutState = (); type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, ()) { let layout_id = self.child.as_mut().unwrap().request_layout(cx); (layout_id, ()) } @@ -38,7 +38,7 @@ impl Element for Deferred { &mut self, _bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let child = self.child.take().unwrap(); let element_offset = cx.element_offset(); @@ -50,7 +50,7 @@ impl Element for Deferred { _bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, _prepaint: &mut Self::PrepaintState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 92c3420206..981b86aadc 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -17,11 +17,11 @@ use crate::{ point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds, - ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox, - HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, - StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext, + ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, Global, Hitbox, HitboxId, + IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, ModifiersChangedEvent, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, + Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, + View, Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -1123,7 +1123,7 @@ impl Element for Div { type RequestLayoutState = DivFrameState; type PrepaintState = Option; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let mut child_layout_ids = SmallVec::new(); let layout_id = self.interactivity.request_layout(cx, |style, cx| { cx.with_text_style(style.text_style().cloned(), |cx| { @@ -1142,7 +1142,7 @@ impl Element for Div { &mut self, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); @@ -1197,7 +1197,7 @@ impl Element for Div { bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.interactivity .paint(bounds, hitbox.as_ref(), cx, |_style, cx| { @@ -1276,8 +1276,8 @@ impl Interactivity { /// Layout this element according to this interactivity state's configured styles pub fn request_layout( &mut self, - cx: &mut ElementContext, - f: impl FnOnce(Style, &mut ElementContext) -> LayoutId, + cx: &mut WindowContext, + f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, ) -> LayoutId { cx.with_element_state::( self.element_id.clone(), @@ -1341,8 +1341,8 @@ impl Interactivity { &mut self, bounds: Bounds, content_size: Size, - cx: &mut ElementContext, - f: impl FnOnce(&Style, Point, Option, &mut ElementContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&Style, Point, Option, &mut WindowContext) -> R, ) -> R { self.content_size = content_size; cx.with_element_state::( @@ -1406,7 +1406,7 @@ impl Interactivity { &mut self, bounds: Bounds, style: &Style, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Point { if let Some(scroll_offset) = self.scroll_offset.as_ref() { if let Some(scroll_handle) = &self.tracked_scroll_handle { @@ -1456,8 +1456,8 @@ impl Interactivity { &mut self, bounds: Bounds, hitbox: Option<&Hitbox>, - cx: &mut ElementContext, - f: impl FnOnce(&Style, &mut ElementContext), + cx: &mut WindowContext, + f: impl FnOnce(&Style, &mut WindowContext), ) { self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(cx)); cx.with_element_state::( @@ -1482,7 +1482,7 @@ impl Interactivity { return ((), element_state); } - style.paint(bounds, cx, |cx: &mut ElementContext| { + style.paint(bounds, cx, |cx: &mut WindowContext| { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| { if let Some(hitbox) = hitbox { @@ -1521,7 +1521,7 @@ impl Interactivity { } #[cfg(debug_assertions)] - fn paint_debug_info(&mut self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) { + fn paint_debug_info(&mut self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) { if self.element_id.is_some() && (style.debug || style.debug_below || cx.has_global::()) && hitbox.is_hovered(cx) @@ -1530,7 +1530,7 @@ impl Interactivity { let element_id = format!("{:?}", self.element_id.as_ref().unwrap()); let str_len = element_id.len(); - let render_debug_text = |cx: &mut ElementContext| { + let render_debug_text = |cx: &mut WindowContext| { if let Some(text) = cx .text_system() .shape_text( @@ -1629,7 +1629,7 @@ impl Interactivity { &mut self, hitbox: &Hitbox, element_state: Option<&mut InteractiveElementState>, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { // If this element can be focused, register a mouse down listener // that will automatically transfer focus when hitting the element. @@ -1712,11 +1712,11 @@ impl Interactivity { let mut can_drop = true; if let Some(predicate) = &can_drop_predicate { - can_drop = predicate(drag.value.as_ref(), cx.deref_mut()); + can_drop = predicate(drag.value.as_ref(), cx); } if can_drop { - listener(drag.value.as_ref(), cx.deref_mut()); + listener(drag.value.as_ref(), cx); cx.refresh(); cx.stop_propagation(); } @@ -1840,7 +1840,7 @@ impl Interactivity { *was_hovered = is_hovered; drop(was_hovered); - hover_listener(&is_hovered, cx.deref_mut()); + hover_listener(&is_hovered, cx); } }); } @@ -1969,7 +1969,7 @@ impl Interactivity { } } - fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) { + fn paint_keyboard_listeners(&mut self, cx: &mut WindowContext) { let key_down_listeners = mem::take(&mut self.key_down_listeners); let key_up_listeners = mem::take(&mut self.key_up_listeners); let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners); @@ -2004,7 +2004,7 @@ impl Interactivity { } } - fn paint_hover_group_handler(&self, cx: &mut ElementContext) { + fn paint_hover_group_handler(&self, cx: &mut WindowContext) { let group_hitbox = self .group_hover_style .as_ref() @@ -2021,7 +2021,7 @@ impl Interactivity { } } - fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) { + fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) { if let Some(scroll_offset) = self.scroll_offset.clone() { let overflow = style.overflow; let line_height = cx.line_height(); @@ -2064,7 +2064,7 @@ impl Interactivity { } /// Compute the visual style for this element, based on the current bounds and the element's state. - pub fn compute_style(&self, hitbox: Option<&Hitbox>, cx: &mut ElementContext) -> Style { + pub fn compute_style(&self, hitbox: Option<&Hitbox>, cx: &mut WindowContext) -> Style { cx.with_element_state(self.element_id.clone(), |element_state, cx| { let mut element_state = element_state.map(|element_state| element_state.unwrap_or_default()); @@ -2078,7 +2078,7 @@ impl Interactivity { &self, hitbox: Option<&Hitbox>, element_state: Option<&mut InteractiveElementState>, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); @@ -2119,7 +2119,7 @@ impl Interactivity { if let Some(drag) = cx.active_drag.take() { let mut can_drop = true; if let Some(can_drop_predicate) = &self.can_drop_predicate { - can_drop = can_drop_predicate(drag.value.as_ref(), cx.deref_mut()); + can_drop = can_drop_predicate(drag.value.as_ref(), cx); } if can_drop { @@ -2264,7 +2264,7 @@ where type RequestLayoutState = E::RequestLayoutState; type PrepaintState = E::PrepaintState; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { self.element.request_layout(cx) } @@ -2272,7 +2272,7 @@ where &mut self, bounds: Bounds, state: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> E::PrepaintState { self.element.prepaint(bounds, state, cx) } @@ -2282,7 +2282,7 @@ where bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.element.paint(bounds, request_layout, prepaint, cx) } @@ -2347,7 +2347,7 @@ where type RequestLayoutState = E::RequestLayoutState; type PrepaintState = E::PrepaintState; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { self.element.request_layout(cx) } @@ -2355,7 +2355,7 @@ where &mut self, bounds: Bounds, state: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> E::PrepaintState { self.element.prepaint(bounds, state, cx) } @@ -2365,7 +2365,7 @@ where bounds: Bounds, request_layout: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.element.paint(bounds, request_layout, prepaint, cx); } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index fad4a49fee..51eeccb3f8 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -3,9 +3,9 @@ use std::path::PathBuf; use std::sync::Arc; use crate::{ - point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element, - ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, - Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext, + point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element, Hitbox, + ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, Pixels, SharedUri, + Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext, }; use futures::{AsyncReadExt, Future}; use image::{ImageBuffer, ImageError}; @@ -232,7 +232,7 @@ impl Element for Img { type RequestLayoutState = (); type PrepaintState = Option; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let layout_id = self.interactivity.request_layout(cx, |mut style, cx| { if let Some(data) = self.source.data(cx) { let image_size = data.size(); @@ -260,7 +260,7 @@ impl Element for Img { &mut self, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { self.interactivity .prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) @@ -271,7 +271,7 @@ impl Element for Img { bounds: Bounds, _: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let source = self.source.clone(); self.interactivity @@ -319,7 +319,7 @@ impl InteractiveElement for Img { } impl ImageSource { - fn data(&self, cx: &mut ElementContext) -> Option> { + fn data(&self, cx: &mut WindowContext) -> Option> { match self { ImageSource::Uri(_) | ImageSource::File(_) => { let uri_or_path: UriOrPath = match self { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index d5caf22955..befee0bbd9 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -8,8 +8,8 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, - Element, ElementContext, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, - Size, Style, StyleRefinement, Styled, WindowContext, + Element, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, + StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; @@ -434,7 +434,7 @@ impl StateInner { available_width: Option, available_height: Pixels, padding: &Edges, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> LayoutItemsResponse { let old_items = self.items.clone(); let mut measured_items = VecDeque::new(); @@ -609,7 +609,7 @@ impl StateInner { bounds: Bounds, padding: Edges, autoscroll: bool, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Result { cx.transact(|cx| { let mut layout_response = @@ -706,7 +706,7 @@ impl Element for List { fn request_layout( &mut self, - cx: &mut crate::ElementContext, + cx: &mut crate::WindowContext, ) -> (crate::LayoutId, Self::RequestLayoutState) { let layout_id = match self.sizing_behavior { ListSizingBehavior::Infer => { @@ -772,7 +772,7 @@ impl Element for List { &mut self, bounds: Bounds, _: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> ListPrepaintState { let state = &mut *self.state.0.borrow_mut(); state.reset = false; @@ -815,7 +815,7 @@ impl Element for List { bounds: Bounds, _: &mut Self::RequestLayoutState, prepaint: &mut Self::PrepaintState, - cx: &mut crate::ElementContext, + cx: &mut crate::WindowContext, ) { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { for item in &mut prepaint.layout.item_layouts { @@ -951,11 +951,9 @@ mod test { }); // Paint - cx.draw( - point(px(0.), px(0.)), - size(px(100.), px(20.)).into(), - |_| list(state.clone()).w_full().h_full().into_any(), - ); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| { + list(state.clone()).w_full().h_full() + }); // Reset state.reset(5); diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index ae2f4c2074..83f9ba68df 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,7 +1,7 @@ use crate::{ - geometry::Negate as _, point, px, radians, size, Bounds, Element, ElementContext, Hitbox, - InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, - Size, StyleRefinement, Styled, TransformationMatrix, + geometry::Negate as _, point, px, radians, size, Bounds, Element, Hitbox, InteractiveElement, + Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size, + StyleRefinement, Styled, TransformationMatrix, WindowContext, }; use util::ResultExt; @@ -40,7 +40,7 @@ impl Element for Svg { type RequestLayoutState = (); type PrepaintState = Option; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let layout_id = self .interactivity .request_layout(cx, |style, cx| cx.request_layout(&style, None)); @@ -51,7 +51,7 @@ impl Element for Svg { &mut self, bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { self.interactivity .prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox) @@ -62,7 +62,7 @@ impl Element for Svg { bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) where Self: Sized, { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 565638bfb4..bdafd809a8 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,8 +1,7 @@ use crate::{ - ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId, - HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, - TOOLTIP_DELAY, + ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, HighlightStyle, + Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, + SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine, TOOLTIP_DELAY, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; @@ -20,7 +19,7 @@ impl Element for &'static str { type RequestLayoutState = TextState; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) @@ -30,7 +29,7 @@ impl Element for &'static str { &mut self, _bounds: Bounds, _text_state: &mut Self::RequestLayoutState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } @@ -39,7 +38,7 @@ impl Element for &'static str { bounds: Bounds, text_state: &mut TextState, _: &mut (), - cx: &mut ElementContext, + cx: &mut WindowContext, ) { text_state.paint(bounds, self, cx) } @@ -65,7 +64,7 @@ impl Element for SharedString { type RequestLayoutState = TextState; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) @@ -75,7 +74,7 @@ impl Element for SharedString { &mut self, _bounds: Bounds, _text_state: &mut Self::RequestLayoutState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } @@ -84,7 +83,7 @@ impl Element for SharedString { bounds: Bounds, text_state: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let text_str: &str = self.as_ref(); text_state.paint(bounds, text_str, cx) @@ -151,7 +150,7 @@ impl Element for StyledText { type RequestLayoutState = TextState; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let mut state = TextState::default(); let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); (layout_id, state) @@ -161,7 +160,7 @@ impl Element for StyledText { &mut self, _bounds: Bounds, _state: &mut Self::RequestLayoutState, - _cx: &mut ElementContext, + _cx: &mut WindowContext, ) { } @@ -170,7 +169,7 @@ impl Element for StyledText { bounds: Bounds, text_state: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { text_state.paint(bounds, &self.text, cx) } @@ -204,7 +203,7 @@ impl TextState { &mut self, text: SharedString, runs: Option>, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> LayoutId { let text_style = cx.text_style(); let font_size = text_style.font_size.to_pixels(cx.rem_size()); @@ -279,7 +278,7 @@ impl TextState { layout_id } - fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut ElementContext) { + fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut WindowContext) { let element_state = self.lock(); let element_state = element_state .as_ref() @@ -405,7 +404,7 @@ impl Element for InteractiveText { type RequestLayoutState = TextState; type PrepaintState = Hitbox; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { self.text.request_layout(cx) } @@ -413,7 +412,7 @@ impl Element for InteractiveText { &mut self, bounds: Bounds, state: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Hitbox { cx.with_element_state::( Some(self.element_id.clone()), @@ -442,7 +441,7 @@ impl Element for InteractiveText { bounds: Bounds, text_state: &mut Self::RequestLayoutState, hitbox: &mut Hitbox, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { cx.with_element_state::( Some(self.element_id.clone()), diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 2813043c7a..910f82bbee 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,9 +5,9 @@ //! elements with uniform height. use crate::{ - point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext, - ElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render, - ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, + point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId, Hitbox, + InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render, ScrollHandle, Size, + StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -107,7 +107,7 @@ impl Element for UniformList { type RequestLayoutState = UniformListFrameState; type PrepaintState = Option; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let max_items = self.item_count; let item_size = self.measure_item(None, cx); let layout_id = self.interactivity.request_layout(cx, |style, cx| { @@ -141,7 +141,7 @@ impl Element for UniformList { &mut self, bounds: Bounds, frame_state: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { let style = self.interactivity.compute_style(None, cx); let border = style.border_widths.to_pixels(cx.rem_size()); @@ -239,7 +239,7 @@ impl Element for UniformList { bounds: Bounds, request_layout: &mut Self::RequestLayoutState, hitbox: &mut Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.interactivity .paint(bounds, hitbox.as_ref(), cx, |_, cx| { @@ -265,7 +265,7 @@ impl UniformList { self } - fn measure_item(&self, list_width: Option, cx: &mut ElementContext) -> Size { + fn measure_item(&self, list_width: Option, cx: &mut WindowContext) -> Size { if self.item_count == 0 { return Size::default(); } diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index ff0b995693..0031ed82c2 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -50,9 +50,8 @@ /// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) /// use crate::{ - Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding, - KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, - WindowContext, + Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext, }; use collections::FxHashMap; use smallvec::SmallVec; @@ -107,8 +106,8 @@ impl ReusedSubtree { } } -type KeyListener = Rc; -type ModifiersChangedListener = Rc; +type KeyListener = Rc; +type ModifiersChangedListener = Rc; #[derive(Clone)] pub(crate) struct DispatchActionListener { diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 9a002d6700..4a7c78d751 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -2,9 +2,9 @@ use std::{iter, mem, ops::Range}; use crate::{ black, phi, point, quad, rems, AbsoluteLength, Bounds, ContentMask, Corners, CornersRefinement, - CursorStyle, DefiniteLength, Edges, EdgesRefinement, ElementContext, Font, FontFeatures, - FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, - SizeRefinement, Styled, TextRun, + CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, + Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, + TextRun, WindowContext, }; use collections::HashSet; use refineable::Refineable; @@ -391,8 +391,8 @@ impl Style { pub fn paint( &self, bounds: Bounds, - cx: &mut ElementContext, - continuation: impl FnOnce(&mut ElementContext), + cx: &mut WindowContext, + continuation: impl FnOnce(&mut WindowContext), ) { #[cfg(debug_assertions)] if self.debug_below { diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 855cfaf37e..a9a52f0757 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -1,6 +1,6 @@ use crate::{ - black, fill, point, px, size, Bounds, ElementContext, Hsla, LineLayout, Pixels, Point, Result, - SharedString, StrikethroughStyle, UnderlineStyle, WrapBoundary, WrappedLineLayout, + black, fill, point, px, size, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, + StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -48,7 +48,7 @@ impl ShapedLine { &self, origin: Point, line_height: Pixels, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Result<()> { paint_line( origin, @@ -86,7 +86,7 @@ impl WrappedLine { &self, origin: Point, line_height: Pixels, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Result<()> { paint_line( origin, @@ -107,7 +107,7 @@ fn paint_line( line_height: Pixels, decoration_runs: &[DecorationRun], wrap_boundaries: &[WrapBoundary], - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Result<()> { let line_bounds = Bounds::new(origin, size(layout.width, line_height)); cx.paint_layer(line_bounds, |cx| { diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 3d9fb82cd5..6b68e5235e 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,8 +1,8 @@ use crate::{ seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, Bounds, ContentMask, Element, - ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, - LayoutId, Model, PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, - TextStyle, ViewContext, VisualContext, WeakModel, + ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, LayoutId, Model, + PaintIndex, Pixels, PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, ViewContext, + VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use refineable::Refineable; @@ -93,7 +93,7 @@ impl Element for View { type RequestLayoutState = AnyElement; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element()); let layout_id = element.request_layout(cx); @@ -105,7 +105,7 @@ impl Element for View { &mut self, _: Bounds, element: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { cx.set_view_id(self.entity_id()); cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { @@ -118,7 +118,7 @@ impl Element for View { _: Bounds, element: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { cx.with_element_id(Some(ElementId::View(self.entity_id())), |cx| { element.paint(cx) @@ -220,7 +220,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - render: fn(&AnyView, &mut ElementContext) -> AnyElement, + render: fn(&AnyView, &mut WindowContext) -> AnyElement, cached_style: Option, } @@ -279,7 +279,7 @@ impl Element for AnyView { type RequestLayoutState = Option; type PrepaintState = Option; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { if let Some(style) = self.cached_style.as_ref() { let mut root_style = Style::default(); root_style.refine(style); @@ -298,7 +298,7 @@ impl Element for AnyView { &mut self, bounds: Bounds, element: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { cx.set_view_id(self.entity_id()); if self.cached_style.is_some() { @@ -359,7 +359,7 @@ impl Element for AnyView { _bounds: Bounds, _: &mut Self::RequestLayoutState, element: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { if self.cached_style.is_some() { cx.with_element_state::( @@ -408,7 +408,7 @@ impl IntoElement for AnyView { /// A weak, dynamically-typed view handle that does not prevent the view from being released. pub struct AnyWeakView { model: AnyWeakModel, - render: fn(&AnyView, &mut ElementContext) -> AnyElement, + render: fn(&AnyView, &mut WindowContext) -> AnyElement, } impl AnyWeakView { @@ -447,11 +447,11 @@ impl std::fmt::Debug for AnyWeakView { } mod any_view { - use crate::{AnyElement, AnyView, ElementContext, IntoElement, Render}; + use crate::{AnyElement, AnyView, IntoElement, Render, WindowContext}; pub(crate) fn render( view: &AnyView, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> AnyElement { let view = view.clone().downcast::().unwrap(); view.update(cx, |view, cx| view.render(cx).into_any_element()) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 3c6160515f..4d20b4a441 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,32 +1,42 @@ use crate::{ - point, px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, - AsyncWindowContext, Bounds, Context, Corners, CursorStyle, DevicePixels, - DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, - EntityId, EventEmitter, FileDropEvent, Flatten, Global, GlobalElementId, Hsla, KeyBinding, - KeyDownEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, - Modifiers, ModifiersChangedEvent, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Render, - ScaledPixels, SharedString, Size, SubscriberSet, Subscription, TaffyLayoutEngine, Task, - TextStyle, TextStyleRefinement, View, VisualContext, WeakView, WindowAppearance, - WindowBackgroundAppearance, WindowOptions, WindowParams, WindowTextSystem, + hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip, + AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, + Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId, + DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, + FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding, + KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent, + LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent, + MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, + SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, + TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, WindowTextSystem, + SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; -use collections::FxHashSet; +use collections::{FxHashMap, FxHashSet}; use derive_more::{Deref, DerefMut}; use futures::channel::oneshot; +use futures::{future::Shared, FutureExt}; +#[cfg(target_os = "macos")] +use media::core_video::CVImageBuffer; use parking_lot::RwLock; use refineable::Refineable; use slotmap::SlotMap; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, - borrow::{Borrow, BorrowMut}, + borrow::{Borrow, BorrowMut, Cow}, cell::{Cell, RefCell}, + cmp, fmt::{Debug, Display}, future::Future, hash::{Hash, Hasher}, marker::PhantomData, mem, + ops::Range, rc::Rc, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -34,12 +44,11 @@ use std::{ }, time::{Duration, Instant}, }; +use util::post_inc; use util::{measure, ResultExt}; -mod element_cx; mod prompts; -pub use element_cx::*; pub use prompts::*; /// Represents the two different phases when dispatching events. @@ -269,6 +278,192 @@ pub struct DismissEvent; type FrameCallback = Box; +pub(crate) type AnyMouseListener = + Box; + +#[derive(Clone)] +pub(crate) struct CursorStyleRequest { + pub(crate) hitbox_id: HitboxId, + pub(crate) style: CursorStyle, +} + +/// An identifier for a [Hitbox]. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct HitboxId(usize); + +impl HitboxId { + /// Checks if the hitbox with this id is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + cx.window.mouse_hit_test.0.contains(self) + } +} + +/// A rectangular region that potentially blocks hitboxes inserted prior. +/// See [WindowContext::insert_hitbox] for more details. +#[derive(Clone, Debug, Deref)] +pub struct Hitbox { + /// A unique identifier for the hitbox. + pub id: HitboxId, + /// The bounds of the hitbox. + #[deref] + pub bounds: Bounds, + /// The content mask when the hitbox was inserted. + pub content_mask: ContentMask, + /// Whether the hitbox occludes other hitboxes inserted prior. + pub opaque: bool, +} + +impl Hitbox { + /// Checks if the hitbox is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + self.id.is_hovered(cx) + } +} + +#[derive(Default, Eq, PartialEq)] +pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); + +/// An identifier for a tooltip. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct TooltipId(usize); + +impl TooltipId { + /// Checks if the tooltip is currently hovered. + pub fn is_hovered(&self, cx: &WindowContext) -> bool { + cx.window + .tooltip_bounds + .as_ref() + .map_or(false, |tooltip_bounds| { + tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position()) + }) + } +} + +pub(crate) struct TooltipBounds { + id: TooltipId, + bounds: Bounds, +} + +#[derive(Clone)] +pub(crate) struct TooltipRequest { + id: TooltipId, + tooltip: AnyTooltip, +} + +pub(crate) struct DeferredDraw { + priority: usize, + parent_node: DispatchNodeId, + element_id_stack: GlobalElementId, + text_style_stack: Vec, + element: Option, + absolute_offset: Point, + prepaint_range: Range, + paint_range: Range, +} + +pub(crate) struct Frame { + pub(crate) focus: Option, + pub(crate) window_active: bool, + pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, + accessed_element_states: Vec<(GlobalElementId, TypeId)>, + pub(crate) mouse_listeners: Vec>, + pub(crate) dispatch_tree: DispatchTree, + pub(crate) scene: Scene, + pub(crate) hitboxes: Vec, + pub(crate) deferred_draws: Vec, + pub(crate) input_handlers: Vec>, + pub(crate) tooltip_requests: Vec>, + pub(crate) cursor_styles: Vec, + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: FxHashMap>, +} + +#[derive(Clone, Default)] +pub(crate) struct PrepaintStateIndex { + hitboxes_index: usize, + tooltips_index: usize, + deferred_draws_index: usize, + dispatch_tree_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +#[derive(Clone, Default)] +pub(crate) struct PaintIndex { + scene_index: usize, + mouse_listeners_index: usize, + input_handlers_index: usize, + cursor_styles_index: usize, + accessed_element_states_index: usize, + line_layout_index: LineLayoutIndex, +} + +impl Frame { + pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + focus: None, + window_active: false, + element_states: FxHashMap::default(), + accessed_element_states: Vec::new(), + mouse_listeners: Vec::new(), + dispatch_tree, + scene: Scene::default(), + hitboxes: Vec::new(), + deferred_draws: Vec::new(), + input_handlers: Vec::new(), + tooltip_requests: Vec::new(), + cursor_styles: Vec::new(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), + } + } + + pub(crate) fn clear(&mut self) { + self.element_states.clear(); + self.accessed_element_states.clear(); + self.mouse_listeners.clear(); + self.dispatch_tree.clear(); + self.scene.clear(); + self.input_handlers.clear(); + self.tooltip_requests.clear(); + self.cursor_styles.clear(); + self.hitboxes.clear(); + self.deferred_draws.clear(); + } + + pub(crate) fn hit_test(&self, position: Point) -> HitTest { + let mut hit_test = HitTest::default(); + for hitbox in self.hitboxes.iter().rev() { + let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); + if bounds.contains(&position) { + hit_test.0.push(hitbox.id); + if hitbox.opaque { + break; + } + } + } + hit_test + } + + pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { + self.focus + .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) + .unwrap_or_default() + } + + pub(crate) fn finish(&mut self, prev_frame: &mut Self) { + for element_state_key in &self.accessed_element_states { + if let Some(element_state) = prev_frame.element_states.remove(element_state_key) { + self.element_states + .insert(element_state_key.clone(), element_state); + } + } + + self.scene.finish(); + } +} + // Holds the state for a specific window. #[doc(hidden)] pub struct Window { @@ -321,7 +516,7 @@ pub struct Window { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum DrawPhase { None, - Layout, + Prepaint, Paint, Focus, } @@ -1039,7 +1234,7 @@ impl<'a> WindowContext<'a> { .push(Some(input_handler)); } - self.with_element_context(|cx| cx.draw_roots()); + self.draw_roots(); self.window.dirty_views.clear(); self.window @@ -1123,6 +1318,1431 @@ impl<'a> WindowContext<'a> { profiling::finish_frame!(); } + fn draw_roots(&mut self) { + self.window.draw_phase = DrawPhase::Prepaint; + self.window.tooltip_bounds.take(); + + // Layout all root elements. + let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any(); + root_element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); + + let mut sorted_deferred_draws = + (0..self.window.next_frame.deferred_draws.len()).collect::>(); + sorted_deferred_draws.sort_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority); + self.prepaint_deferred_draws(&sorted_deferred_draws); + + let mut prompt_element = None; + let mut active_drag_element = None; + let mut tooltip_element = None; + if let Some(prompt) = self.window.prompt.take() { + let mut element = prompt.view.any_view().into_any(); + element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); + prompt_element = Some(element); + self.window.prompt = Some(prompt); + } else if let Some(active_drag) = self.app.active_drag.take() { + let mut element = active_drag.view.clone().into_any(); + let offset = self.mouse_position() - active_drag.cursor_offset; + element.prepaint_as_root(offset, AvailableSpace::min_size(), self); + active_drag_element = Some(element); + self.app.active_drag = Some(active_drag); + } else { + tooltip_element = self.prepaint_tooltip(); + } + + self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position); + + // Now actually paint the elements. + self.window.draw_phase = DrawPhase::Paint; + root_element.paint(self); + + self.paint_deferred_draws(&sorted_deferred_draws); + + if let Some(mut prompt_element) = prompt_element { + prompt_element.paint(self) + } else if let Some(mut drag_element) = active_drag_element { + drag_element.paint(self); + } else if let Some(mut tooltip_element) = tooltip_element { + tooltip_element.paint(self); + } + } + + fn prepaint_tooltip(&mut self) -> Option { + let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?; + let tooltip_request = tooltip_request.unwrap(); + let mut element = tooltip_request.tooltip.view.clone().into_any(); + let mouse_position = tooltip_request.tooltip.mouse_position; + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); + + let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); + let window_bounds = Bounds { + origin: Point::default(), + size: self.viewport_size(), + }; + + if tooltip_bounds.right() > window_bounds.right() { + let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); + if new_x >= Pixels::ZERO { + tooltip_bounds.origin.x = new_x; + } else { + tooltip_bounds.origin.x = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), + ); + } + } + + if tooltip_bounds.bottom() > window_bounds.bottom() { + let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); + if new_y >= Pixels::ZERO { + tooltip_bounds.origin.y = new_y; + } else { + tooltip_bounds.origin.y = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), + ); + } + } + + self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); + + self.window.tooltip_bounds = Some(TooltipBounds { + id: tooltip_request.id, + bounds: tooltip_bounds, + }); + Some(element) + } + + fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window.element_id_stack = deferred_draw.element_id_stack.clone(); + self.window.text_style_stack = deferred_draw.text_style_stack.clone(); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let prepaint_start = self.prepaint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| { + element.prepaint(cx) + }); + } else { + self.reuse_prepaint(deferred_draw.prepaint_range.clone()); + } + let prepaint_end = self.prepaint_index(); + deferred_draw.prepaint_range = prepaint_start..prepaint_end; + } + assert_eq!( + self.window.next_frame.deferred_draws.len(), + 0, + "cannot call defer_draw during deferred drawing" + ); + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + self.window.text_style_stack.clear(); + } + + fn paint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { + assert_eq!(self.window.element_id_stack.len(), 0); + + let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); + for deferred_draw_ix in deferred_draw_indices { + let mut deferred_draw = &mut deferred_draws[*deferred_draw_ix]; + self.window.element_id_stack = deferred_draw.element_id_stack.clone(); + self.window + .next_frame + .dispatch_tree + .set_active_node(deferred_draw.parent_node); + + let paint_start = self.paint_index(); + if let Some(element) = deferred_draw.element.as_mut() { + element.paint(self); + } else { + self.reuse_paint(deferred_draw.paint_range.clone()); + } + let paint_end = self.paint_index(); + deferred_draw.paint_range = paint_start..paint_end; + } + self.window.next_frame.deferred_draws = deferred_draws; + self.window.element_id_stack.clear(); + } + + pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { + PrepaintStateIndex { + hitboxes_index: self.window.next_frame.hitboxes.len(), + tooltips_index: self.window.next_frame.tooltip_requests.len(), + deferred_draws_index: self.window.next_frame.deferred_draws.len(), + dispatch_tree_index: self.window.next_frame.dispatch_tree.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_prepaint(&mut self, range: Range) { + let window = &mut self.window; + window.next_frame.hitboxes.extend( + window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] + .iter() + .cloned(), + ); + window.next_frame.tooltip_requests.extend( + window.rendered_frame.tooltip_requests + [range.start.tooltips_index..range.end.tooltips_index] + .iter_mut() + .map(|request| request.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() + .cloned(), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + + let reused_subtree = window.next_frame.dispatch_tree.reuse_subtree( + range.start.dispatch_tree_index..range.end.dispatch_tree_index, + &mut window.rendered_frame.dispatch_tree, + ); + window.next_frame.deferred_draws.extend( + window.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + prepaint_range: deferred_draw.prepaint_range.clone(), + paint_range: deferred_draw.paint_range.clone(), + }), + ); + } + + pub(crate) fn paint_index(&self) -> PaintIndex { + PaintIndex { + scene_index: self.window.next_frame.scene.len(), + mouse_listeners_index: self.window.next_frame.mouse_listeners.len(), + input_handlers_index: self.window.next_frame.input_handlers.len(), + cursor_styles_index: self.window.next_frame.cursor_styles.len(), + accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), + line_layout_index: self.window.text_system.layout_index(), + } + } + + pub(crate) fn reuse_paint(&mut self, range: Range) { + let window = &mut self.window; + + window.next_frame.cursor_styles.extend( + window.rendered_frame.cursor_styles + [range.start.cursor_styles_index..range.end.cursor_styles_index] + .iter() + .cloned(), + ); + window.next_frame.input_handlers.extend( + window.rendered_frame.input_handlers + [range.start.input_handlers_index..range.end.input_handlers_index] + .iter_mut() + .map(|handler| handler.take()), + ); + window.next_frame.mouse_listeners.extend( + window.rendered_frame.mouse_listeners + [range.start.mouse_listeners_index..range.end.mouse_listeners_index] + .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() + .cloned(), + ); + window + .text_system + .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); + window.next_frame.scene.replay( + range.start.scene_index..range.end.scene_index, + &window.rendered_frame.scene, + ); + } + + /// Push a text style onto the stack, and call a function with that style active. + /// Use [`AppContext::text_style`] to get the current, combined text style. This method + /// should only be called as part of element drawing. + pub fn with_text_style(&mut self, style: Option, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + if let Some(style) = style { + self.window.text_style_stack.push(style); + let result = f(self); + self.window.text_style_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the cursor style at the platform level. This method should only be called + /// during the prepaint phase of element drawing. + pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .cursor_styles + .push(CursorStyleRequest { + hitbox_id: hitbox.id, + style, + }); + } + + /// Sets a tooltip to be rendered for the upcoming frame. This method should only be called + /// during the paint phase of element drawing. + pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0)); + self.window + .next_frame + .tooltip_requests + .push(Some(TooltipRequest { id, tooltip })); + id + } + + /// Pushes the given element id onto the global stack and invokes the given closure + /// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor + /// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is + /// used to associate state with identified elements across separate frames. This method should + /// only be called as part of element drawing. + pub fn with_element_id( + &mut self, + id: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + if let Some(id) = id.map(Into::into) { + let window = self.window_mut(); + window.element_id_stack.push(id); + let result = f(self); + let window: &mut Window = self.borrow_mut(); + window.element_id_stack.pop(); + result + } else { + f(self) + } + } + + /// Invoke the given function with the given content mask after intersecting it + /// with the current mask. This method should only be called during element drawing. + pub fn with_content_mask( + &mut self, + mask: Option>, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.window_mut().content_mask_stack.push(mask); + let result = f(self); + self.window_mut().content_mask_stack.pop(); + result + } else { + f(self) + } + } + + /// Updates the global element offset relative to the current offset. This is used to implement + /// scrolling. This method should only be called during the prepaint phase of element drawing. + pub fn with_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + if offset.is_zero() { + return f(self); + }; + + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Updates the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. This method should only be called during + /// the prepaint phase of element drawing. + pub fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + self.window_mut().element_offset_stack.push(offset); + let result = f(self); + self.window_mut().element_offset_stack.pop(); + result + } + + /// Perform prepaint on child elements in a "retryable" manner, so that any side effects + /// of prepaints can be discarded before prepainting again. This is used to support autoscroll + /// where we need to prepaint children to detect the autoscroll bounds, then adjust the + /// element offset and prepaint again. See [`List`] for an example. This method should only be + /// called during the prepaint phase of element drawing. + pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + let index = self.prepaint_index(); + let result = f(self); + if result.is_err() { + self.window + .next_frame + .hitboxes + .truncate(index.hitboxes_index); + self.window + .next_frame + .tooltip_requests + .truncate(index.tooltips_index); + self.window + .next_frame + .deferred_draws + .truncate(index.deferred_draws_index); + self.window + .next_frame + .dispatch_tree + .truncate(index.dispatch_tree_index); + self.window + .next_frame + .accessed_element_states + .truncate(index.accessed_element_states_index); + self.window + .text_system + .truncate_layouts(index.line_layout_index); + } + result + } + + /// When you call this method during [`prepaint`], containing elements will attempt to + /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call + /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element + /// that supports this method being called on the elements it contains. This method should only be + /// called during the prepaint phase of element drawing. + pub fn request_autoscroll(&mut self, bounds: Bounds) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.requested_autoscroll = Some(bounds); + } + + /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior + /// described in [`request_autoscroll`]. + pub fn take_autoscroll(&mut self) -> Option> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.requested_autoscroll.take() + } + + /// Remove an asset from GPUI's cache + pub fn remove_cached_asset( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.remove::(source) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call. + /// The results of that call will be cached, and returned on subsequent uses of this API. + /// + /// Use [Self::remove_cached_asset] to reload your asset. + pub fn use_cached_asset( + &mut self, + source: &A::Source, + ) -> Option { + self.asset_cache.get::(source).or_else(|| { + if let Some(asset) = self.use_asset::(source) { + self.asset_cache + .insert::(source.to_owned(), asset.clone()); + Some(asset) + } else { + None + } + }) + } + + /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. + /// Your view will be re-drawn once the asset has finished loading. + /// + /// Note that the multiple calls to this method will only result in one `Asset::load` call at a + /// time. + /// + /// This asset will not be cached by default, see [Self::use_cached_asset] + pub fn use_asset(&mut self, source: &A::Source) -> Option { + let asset_id = (TypeId::of::(), hash(source)); + let mut is_first = false; + let task = self + .loading_assets + .remove(&asset_id) + .map(|boxed_task| *boxed_task.downcast::>>().unwrap()) + .unwrap_or_else(|| { + is_first = true; + let future = A::load(source.clone(), self); + let task = self.background_executor().spawn(future).shared(); + task + }); + + task.clone().now_or_never().or_else(|| { + if is_first { + let parent_id = self.parent_view_id(); + self.spawn({ + let task = task.clone(); + |mut cx| async move { + task.await; + + cx.on_next_frame(move |cx| { + if let Some(parent_id) = parent_id { + cx.notify(parent_id) + } else { + cx.refresh() + } + }); + } + }) + .detach(); + } + + self.loading_assets.insert(asset_id, Box::new(task)); + + None + }) + } + + /// Obtain the current element offset. This method should only be called during the + /// prepaint phase of element drawing. + pub fn element_offset(&self) -> Point { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window() + .element_offset_stack + .last() + .copied() + .unwrap_or_default() + } + + /// Obtain the current content mask. This method should only be called during element drawing. + pub fn content_mask(&self) -> ContentMask { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during prepaint, or paint" + ); + self.window() + .content_mask_stack + .last() + .cloned() + .unwrap_or_else(|| ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }) + } + + /// Updates or initializes state for an element with the given id that lives across multiple + /// frames. If an element with this ID existed in the rendered frame, its state will be passed + /// to the given closure. The state returned by the closure will be stored so it can be referenced + /// when drawing the next frame. This method should only be called as part of element drawing. + pub fn with_element_state( + &mut self, + element_id: Option, + f: impl FnOnce(Option>, &mut Self) -> (R, Option), + ) -> R + where + S: 'static, + { + debug_assert!( + matches!( + self.window.draw_phase, + DrawPhase::Prepaint | DrawPhase::Paint + ), + "this method can only be called during request_layout, prepaint, or paint" + ); + let id_is_none = element_id.is_none(); + self.with_element_id(element_id, |cx| { + if id_is_none { + let (result, state) = f(None, cx); + debug_assert!(state.is_none(), "you must not return an element state when passing None for the element id"); + result + } else { + let global_id = cx.window().element_id_stack.clone(); + let key = (global_id, TypeId::of::()); + cx.window.next_frame.accessed_element_states.push(key.clone()); + + if let Some(any) = cx + .window_mut() + .next_frame + .element_states + .remove(&key) + .or_else(|| { + cx.window_mut() + .rendered_frame + .element_states + .remove(&key) + }) + { + let ElementStateBox { + inner, + #[cfg(debug_assertions)] + type_name + } = any; + // Using the extra inner option to avoid needing to reallocate a new box. + let mut state_box = inner + .downcast::>() + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow::anyhow!( + "invalid element state type for id, requested_type {:?}, actual type: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow::anyhow!( + "invalid element state type for id, requested_type {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + // Actual: Option <- View + // Requested: () <- AnyElement + let state = state_box + .take() + .expect("reentrant call to with_element_state for the same state type and element id"); + let (result, state) = f(Some(Some(state)), cx); + state_box.replace(state.expect("you must return ")); + cx.window_mut() + .next_frame + .element_states + .insert(key, ElementStateBox { + inner: state_box, + #[cfg(debug_assertions)] + type_name + }); + result + } else { + let (result, state) = f(Some(None), cx); + cx.window_mut() + .next_frame + .element_states + .insert(key, + ElementStateBox { + inner: Box::new(Some(state.expect("you must return Some when you pass some element id"))), + #[cfg(debug_assertions)] + type_name: std::any::type_name::() + } + + ); + result + } + } + }) + } + + /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree + /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, + /// with higher values being drawn on top. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn defer_draw( + &mut self, + element: AnyElement, + absolute_offset: Point, + priority: usize, + ) { + let window = &mut self.window; + debug_assert_eq!( + window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout or prepaint" + ); + let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap(); + window.next_frame.deferred_draws.push(DeferredDraw { + parent_node, + element_id_stack: window.element_id_stack.clone(), + text_style_stack: window.text_style_stack.clone(), + priority, + element: Some(element), + absolute_offset, + prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), + paint_range: PaintIndex::default()..PaintIndex::default(), + }); + } + + /// Creates a new painting layer for the specified bounds. A "layer" is a batch + /// of geometry that are non-overlapping and have the same draw order. This is typically used + /// for performance reasons. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + let clipped_bounds = bounds.intersect(&content_mask.bounds); + if !clipped_bounds.is_empty() { + self.window + .next_frame + .scene + .push_layer(clipped_bounds.scale(scale_factor)); + } + + let result = f(self); + + if !clipped_bounds.is_empty() { + self.window.next_frame.scene.pop_layer(); + } + + result + } + + /// Paint one or more drop shadows into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_shadows( + &mut self, + bounds: Bounds, + corner_radii: Corners, + shadows: &[BoxShadow], + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + for shadow in shadows { + let mut shadow_bounds = bounds; + shadow_bounds.origin += shadow.offset; + shadow_bounds.dilate(shadow.spread_radius); + self.window.next_frame.scene.insert_primitive(Shadow { + order: 0, + blur_radius: shadow.blur_radius.scale(scale_factor), + bounds: shadow_bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + corner_radii: corner_radii.scale(scale_factor), + color: shadow.color, + }); + } + } + + /// Paint one or more quads into the scene for the next frame at the current stacking context. + /// Quads are colored rectangular regions with an optional background, border, and corner radius. + /// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_quad(&mut self, quad: PaintQuad) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + self.window.next_frame.scene.insert_primitive(Quad { + order: 0, + pad: 0, + bounds: quad.bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + background: quad.background, + border_color: quad.border_color, + corner_radii: quad.corner_radii.scale(scale_factor), + border_widths: quad.border_widths.scale(scale_factor), + }); + } + + /// Paint the given `Path` into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_path(&mut self, mut path: Path, color: impl Into) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let content_mask = self.content_mask(); + path.content_mask = content_mask; + path.color = color.into(); + self.window + .next_frame + .scene + .insert_primitive(path.scale(scale_factor)); + } + + /// Paint an underline into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_underline( + &mut self, + origin: Point, + width: Pixels, + style: &UnderlineStyle, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let height = if style.wavy { + style.thickness * 3. + } else { + style.thickness + }; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + color: style.color.unwrap_or_default(), + thickness: style.thickness.scale(scale_factor), + wavy: style.wavy, + }); + } + + /// Paint a strikethrough into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_strikethrough( + &mut self, + origin: Point, + width: Pixels, + style: &StrikethroughStyle, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let height = style.thickness; + let bounds = Bounds { + origin, + size: size(width, height), + }; + let content_mask = self.content_mask(); + + self.window.next_frame.scene.insert_primitive(Underline { + order: 0, + pad: 0, + bounds: bounds.scale(scale_factor), + content_mask: content_mask.scale(scale_factor), + thickness: style.thickness.scale(scale_factor), + color: style.color.unwrap_or_default(), + wavy: false, + }); + } + + /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single glyph that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_glyph( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + color: Hsla, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let subpixel_variant = Point { + x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, + }; + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + subpixel_variant, + scale_factor, + is_emoji: false, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color, + tile, + transformation: TransformationMatrix::unit(), + }); + } + Ok(()) + } + + /// Paints an emoji glyph into the scene for the next frame at the current z-index. + /// + /// The y component of the origin is the baseline of the glyph. + /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or + /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). + /// This method is only useful if you need to paint a single emoji that has already been shaped. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_emoji( + &mut self, + origin: Point, + font_id: FontId, + glyph_id: GlyphId, + font_size: Pixels, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let glyph_origin = origin.scale(scale_factor); + let params = RenderGlyphParams { + font_id, + glyph_id, + font_size, + // We don't render emojis with subpixel variants. + subpixel_variant: Default::default(), + scale_factor, + is_emoji: true, + }; + + let raster_bounds = self.text_system().raster_bounds(¶ms)?; + if !raster_bounds.is_zero() { + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; + Ok((size, Cow::Owned(bytes))) + })?; + let bounds = Bounds { + origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), + size: tile.bounds.size.map(Into::into), + }; + let content_mask = self.content_mask().scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { + order: 0, + grayscale: false, + bounds, + corner_radii: Default::default(), + content_mask, + tile, + }); + } + Ok(()) + } + + /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_svg( + &mut self, + bounds: Bounds, + path: SharedString, + transformation: TransformationMatrix, + color: Hsla, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + // Render the SVG at twice the size to get a higher quality result. + let params = RenderSvgParams { + path, + size: bounds + .size + .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)), + }; + + let tile = + self.window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + let bytes = self.svg_renderer.render(¶ms)?; + Ok((params.size, Cow::Owned(bytes))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(MonochromeSprite { + order: 0, + pad: 0, + bounds, + content_mask, + color, + tile, + transformation, + }); + + Ok(()) + } + + /// Paint an image into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn paint_image( + &mut self, + bounds: Bounds, + corner_radii: Corners, + data: Arc, + grayscale: bool, + ) -> Result<()> { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let params = RenderImageParams { image_id: data.id }; + + let tile = self + .window + .sprite_atlas + .get_or_insert_with(¶ms.clone().into(), &mut || { + Ok((data.size(), Cow::Borrowed(data.as_bytes()))) + })?; + let content_mask = self.content_mask().scale(scale_factor); + let corner_radii = corner_radii.scale(scale_factor); + + self.window + .next_frame + .scene + .insert_primitive(PolychromeSprite { + order: 0, + grayscale, + bounds, + content_mask, + corner_radii, + tile, + }); + Ok(()) + } + + /// Paint a surface into the scene for the next frame at the current z-index. + /// + /// This method should only be called as part of the paint phase of element drawing. + #[cfg(target_os = "macos")] + pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + let scale_factor = self.scale_factor(); + let bounds = bounds.scale(scale_factor); + let content_mask = self.content_mask().scale(scale_factor); + self.window + .next_frame + .scene + .insert_primitive(crate::Surface { + order: 0, + bounds, + content_mask, + image_buffer, + }); + } + + #[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 + /// calls to the [`Element::request_layout`] trait method and enables any element to participate in layout. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_layout( + &mut self, + style: &Style, + children: impl IntoIterator, + ) -> LayoutId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + self.app.layout_id_buffer.clear(); + self.app.layout_id_buffer.extend(children); + let rem_size = self.rem_size(); + + self.window.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + &self.app.layout_id_buffer, + ) + } + + /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, + /// this variant takes a function that is invoked during layout so you can use arbitrary logic to + /// determine the element's size. One place this is used internally is when measuring text. + /// + /// The given closure is invoked at layout time with the known dimensions and available space and + /// returns a `Size`. + /// + /// This method should only be called as part of the request_layout or prepaint phase of element drawing. + pub fn request_measured_layout< + F: FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, + >( + &mut self, + style: Style, + measure: F, + ) -> LayoutId { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + let rem_size = self.rem_size(); + self.window + .layout_engine + .as_mut() + .unwrap() + .request_measured_layout(style, rem_size, measure) + } + + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, or prepaint" + ); + + let mut layout_engine = self.window.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self); + self.window.layout_engine = Some(layout_engine); + } + + /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by + /// GPUI itself automatically in order to pass your element its `Bounds` automatically. + /// + /// This method should only be called as part of element drawing. + pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during request_layout, prepaint, or paint" + ); + + let mut bounds = self + .window + .layout_engine + .as_mut() + .unwrap() + .layout_bounds(layout_id) + .map(Into::into); + bounds.origin += self.element_offset(); + bounds + } + + /// This method should be called during `prepaint`. You can use + /// the returned [Hitbox] during `paint` or in an event handler + /// to determine whether the inserted hitbox was the topmost. + /// + /// This method should only be called as part of the prepaint phase of element drawing. + pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + + let content_mask = self.content_mask(); + let window = &mut self.window; + let id = window.next_hitbox_id; + window.next_hitbox_id.0 += 1; + let hitbox = Hitbox { + id, + bounds, + content_mask, + opaque, + }; + window.next_frame.hitboxes.push(hitbox.clone()); + hitbox + } + + /// Sets the key context for the current element. This context will be used to translate + /// keybindings into actions. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_key_context(&mut self, context: KeyContext) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .dispatch_tree + .set_key_context(context); + } + + /// Sets the focus handle for the current element. This handle will be used to manage focus state + /// and keyboard event dispatch for the element. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + self.window + .next_frame + .dispatch_tree + .set_focus_id(focus_handle.id); + } + + /// Sets the view id for the current element, which will be used to manage view caching. + /// + /// This method should only be called as part of element prepaint. We plan on removing this + /// method eventually when we solve some issues that require us to construct editor elements + /// directly instead of always using editors via views. + pub fn set_view_id(&mut self, view_id: EntityId) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Prepaint, + "this method can only be called during prepaint" + ); + self.window.next_frame.dispatch_tree.set_view_id(view_id); + } + + /// Get the last view id for the current element + pub fn parent_view_id(&mut self) -> Option { + self.window.next_frame.dispatch_tree.parent_view_id() + } + + /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. This handler will be active for the upcoming frame until the following frame is + /// rendered. + /// + /// This method should only be called as part of the paint phase of element drawing. + /// + /// [element_input_handler]: crate::ElementInputHandler + pub fn handle_input(&mut self, focus_handle: &FocusHandle, input_handler: impl InputHandler) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + if focus_handle.is_focused(self) { + let cx = self.to_async(); + self.window + .next_frame + .input_handlers + .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); + } + } + + /// Register a mouse event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_mouse_event( + &mut self, + mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window.next_frame.mouse_listeners.push(Some(Box::new( + move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| { + if let Some(event) = event.downcast_ref() { + handler(event, phase, cx) + } + }, + ))); + } + + /// Register a key event listener on the window for the next frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_key_event( + &mut self, + listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window.next_frame.dispatch_tree.on_key_event(Rc::new( + move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| { + if let Some(event) = event.downcast_ref::() { + listener(event, phase, cx) + } + }, + )); + } + + /// Register a modifiers changed event listener on the window for the next frame. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + /// + /// This method should only be called as part of the paint phase of element drawing. + pub fn on_modifiers_changed( + &mut self, + listener: impl Fn(&ModifiersChangedEvent, &mut WindowContext) + 'static, + ) { + debug_assert_eq!( + self.window.draw_phase, + DrawPhase::Paint, + "this method can only be called during paint" + ); + + self.window + .next_frame + .dispatch_tree + .on_modifiers_changed(Rc::new( + move |event: &ModifiersChangedEvent, cx: &mut WindowContext<'_>| { + listener(event, cx) + }, + )); + } + fn reset_cursor_style(&self) { // Set the cursor only if we're the active window. if self.is_window_active() { @@ -1282,28 +2902,28 @@ impl<'a> WindowContext<'a> { } let mut mouse_listeners = mem::take(&mut self.window.rendered_frame.mouse_listeners); - self.with_element_context(|cx| { - // Capture phase, events bubble from back to front. Handlers for this phase are used for - // special purposes, such as detecting events outside of a given Bounds. - for listener in &mut mouse_listeners { + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for listener in &mut mouse_listeners { + let listener = listener.as_mut().unwrap(); + listener(event, DispatchPhase::Capture, self); + if !self.app.propagate_event { + break; + } + } + + // Bubble phase, where most normal handlers do their work. + if self.app.propagate_event { + for listener in mouse_listeners.iter_mut().rev() { let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Capture, cx); - if !cx.app.propagate_event { + listener(event, DispatchPhase::Bubble, self); + if !self.app.propagate_event { break; } } + } - // Bubble phase, where most normal handlers do their work. - if cx.app.propagate_event { - for listener in mouse_listeners.iter_mut().rev() { - let listener = listener.as_mut().unwrap(); - listener(event, DispatchPhase::Bubble, cx); - if !cx.app.propagate_event { - break; - } - } - } - }); self.window.rendered_frame.mouse_listeners = mouse_listeners; if self.has_active_drag() { @@ -1425,9 +3045,7 @@ impl<'a> WindowContext<'a> { let node = self.window.rendered_frame.dispatch_tree.node(*node_id); for key_listener in node.key_listeners.clone() { - self.with_element_context(|cx| { - key_listener(event, DispatchPhase::Capture, cx); - }); + key_listener(event, DispatchPhase::Capture, self); if !self.propagate_event { return; } @@ -1439,9 +3057,7 @@ impl<'a> WindowContext<'a> { // Handle low level key events let node = self.window.rendered_frame.dispatch_tree.node(*node_id); for key_listener in node.key_listeners.clone() { - self.with_element_context(|cx| { - key_listener(event, DispatchPhase::Bubble, cx); - }); + key_listener(event, DispatchPhase::Bubble, self); if !self.propagate_event { return; } @@ -1460,9 +3076,7 @@ impl<'a> WindowContext<'a> { for node_id in dispatch_path.iter().rev() { let node = self.window.rendered_frame.dispatch_tree.node(*node_id); for listener in node.modifiers_changed_listeners.clone() { - self.with_element_context(|cx| { - listener(event, cx); - }); + listener(event, self); if !self.propagate_event { return; } @@ -1574,9 +3188,7 @@ impl<'a> WindowContext<'a> { { let any_action = action.as_any(); if action_type == any_action.type_id() { - self.with_element_context(|cx| { - listener(any_action, DispatchPhase::Capture, cx); - }); + listener(any_action, DispatchPhase::Capture, self); if !self.propagate_event { return; @@ -1596,10 +3208,7 @@ impl<'a> WindowContext<'a> { let any_action = action.as_any(); if action_type == any_action.type_id() { self.propagate_event = false; // Actions stop propagation by default during the bubble phase - - self.with_element_context(|cx| { - listener(any_action, DispatchPhase::Bubble, cx); - }); + listener(any_action, DispatchPhase::Bubble, self); if !self.propagate_event { return; @@ -2895,7 +4504,7 @@ impl From<(&'static str, u64)> for ElementId { } /// A rectangle to be rendered in the window at the given position and size. -/// Passed as an argument [`ElementContext::paint_quad`]. +/// Passed as an argument [`WindowContext::paint_quad`]. #[derive(Clone)] pub struct PaintQuad { /// The bounds of the quad within the window. diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs deleted file mode 100644 index 22dc083a19..0000000000 --- a/crates/gpui/src/window/element_cx.rs +++ /dev/null @@ -1,1554 +0,0 @@ -//! The element context is the main interface for interacting with the frame during a paint. -//! -//! Elements are hierarchical and with a few exceptions the context accumulates state in a stack -//! as it processes all of the elements in the frame. The methods that interact with this stack -//! are generally marked with `with_*`, and take a callback to denote the region of code that -//! should be executed with that state. -//! -//! The other main interface is the `paint_*` family of methods, which push basic drawing commands -//! to the GPU. Everything in a GPUI app is drawn with these methods. -//! -//! There are also several internal methods that GPUI uses, such as [`ElementContext::with_element_state`] -//! to call the paint and layout methods on elements. These have been included as they're often useful -//! for taking manual control of the layouting or painting of specialized elements. - -use std::{ - any::{Any, TypeId}, - borrow::{Borrow, BorrowMut, Cow}, - cmp, mem, - ops::Range, - rc::Rc, - sync::Arc, -}; - -use anyhow::Result; -use collections::FxHashMap; -use derive_more::{Deref, DerefMut}; -use futures::{future::Shared, FutureExt}; -#[cfg(target_os = "macos")] -use media::core_video::CVImageBuffer; -use smallvec::SmallVec; -use util::post_inc; - -use crate::{ - hash, point, prelude::*, px, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, - Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, - DispatchPhase, DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, - FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, - KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, - PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, - RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, - StrikethroughStyle, Style, Task, TextStyleRefinement, TransformationMatrix, Underline, - UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS, -}; - -pub(crate) type AnyMouseListener = - Box; - -#[derive(Clone)] -pub(crate) struct CursorStyleRequest { - pub(crate) hitbox_id: HitboxId, - pub(crate) style: CursorStyle, -} - -/// An identifier for a [Hitbox]. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct HitboxId(usize); - -impl HitboxId { - /// Checks if the hitbox with this id is currently hovered. - pub fn is_hovered(&self, cx: &WindowContext) -> bool { - cx.window.mouse_hit_test.0.contains(self) - } -} - -/// A rectangular region that potentially blocks hitboxes inserted prior. -/// See [ElementContext::insert_hitbox] for more details. -#[derive(Clone, Debug, Deref)] -pub struct Hitbox { - /// A unique identifier for the hitbox. - pub id: HitboxId, - /// The bounds of the hitbox. - #[deref] - pub bounds: Bounds, - /// The content mask when the hitbox was inserted. - pub content_mask: ContentMask, - /// Whether the hitbox occludes other hitboxes inserted prior. - pub opaque: bool, -} - -impl Hitbox { - /// Checks if the hitbox is currently hovered. - pub fn is_hovered(&self, cx: &WindowContext) -> bool { - self.id.is_hovered(cx) - } -} - -#[derive(Default, Eq, PartialEq)] -pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>); - -/// An identifier for a tooltip. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct TooltipId(usize); - -impl TooltipId { - /// Checks if the tooltip is currently hovered. - pub fn is_hovered(&self, cx: &WindowContext) -> bool { - cx.window - .tooltip_bounds - .as_ref() - .map_or(false, |tooltip_bounds| { - tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position()) - }) - } -} - -pub(crate) struct TooltipBounds { - id: TooltipId, - bounds: Bounds, -} - -#[derive(Clone)] -pub(crate) struct TooltipRequest { - id: TooltipId, - tooltip: AnyTooltip, -} - -pub(crate) struct DeferredDraw { - priority: usize, - parent_node: DispatchNodeId, - element_id_stack: GlobalElementId, - text_style_stack: Vec, - element: Option, - absolute_offset: Point, - prepaint_range: Range, - paint_range: Range, -} - -pub(crate) struct Frame { - pub(crate) focus: Option, - pub(crate) window_active: bool, - pub(crate) element_states: FxHashMap<(GlobalElementId, TypeId), ElementStateBox>, - accessed_element_states: Vec<(GlobalElementId, TypeId)>, - pub(crate) mouse_listeners: Vec>, - pub(crate) dispatch_tree: DispatchTree, - pub(crate) scene: Scene, - pub(crate) hitboxes: Vec, - pub(crate) deferred_draws: Vec, - pub(crate) input_handlers: Vec>, - pub(crate) tooltip_requests: Vec>, - pub(crate) cursor_styles: Vec, - #[cfg(any(test, feature = "test-support"))] - pub(crate) debug_bounds: FxHashMap>, -} - -#[derive(Clone, Default)] -pub(crate) struct PrepaintStateIndex { - hitboxes_index: usize, - tooltips_index: usize, - deferred_draws_index: usize, - dispatch_tree_index: usize, - accessed_element_states_index: usize, - line_layout_index: LineLayoutIndex, -} - -#[derive(Clone, Default)] -pub(crate) struct PaintIndex { - scene_index: usize, - mouse_listeners_index: usize, - input_handlers_index: usize, - cursor_styles_index: usize, - accessed_element_states_index: usize, - line_layout_index: LineLayoutIndex, -} - -impl Frame { - pub(crate) fn new(dispatch_tree: DispatchTree) -> Self { - Frame { - focus: None, - window_active: false, - element_states: FxHashMap::default(), - accessed_element_states: Vec::new(), - mouse_listeners: Vec::new(), - dispatch_tree, - scene: Scene::default(), - hitboxes: Vec::new(), - deferred_draws: Vec::new(), - input_handlers: Vec::new(), - tooltip_requests: Vec::new(), - cursor_styles: Vec::new(), - - #[cfg(any(test, feature = "test-support"))] - debug_bounds: FxHashMap::default(), - } - } - - pub(crate) fn clear(&mut self) { - self.element_states.clear(); - self.accessed_element_states.clear(); - self.mouse_listeners.clear(); - self.dispatch_tree.clear(); - self.scene.clear(); - self.input_handlers.clear(); - self.tooltip_requests.clear(); - self.cursor_styles.clear(); - self.hitboxes.clear(); - self.deferred_draws.clear(); - } - - pub(crate) fn hit_test(&self, position: Point) -> HitTest { - let mut hit_test = HitTest::default(); - for hitbox in self.hitboxes.iter().rev() { - let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds); - if bounds.contains(&position) { - hit_test.0.push(hitbox.id); - if hitbox.opaque { - break; - } - } - } - hit_test - } - - pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> { - self.focus - .map(|focus_id| self.dispatch_tree.focus_path(focus_id)) - .unwrap_or_default() - } - - pub(crate) fn finish(&mut self, prev_frame: &mut Self) { - for element_state_key in &self.accessed_element_states { - if let Some(element_state) = prev_frame.element_states.remove(element_state_key) { - self.element_states - .insert(element_state_key.clone(), element_state); - } - } - - self.scene.finish(); - } -} - -/// This context is used for assisting in the implementation of the element trait -#[derive(Deref, DerefMut)] -pub struct ElementContext<'a> { - pub(crate) cx: WindowContext<'a>, -} - -impl<'a> WindowContext<'a> { - /// Convert this window context into an ElementContext in this callback. - /// If you need to use this method, you're probably intermixing the imperative - /// and declarative APIs, which is not recommended. - pub fn with_element_context(&mut self, f: impl FnOnce(&mut ElementContext) -> R) -> R { - f(&mut ElementContext { - cx: WindowContext::new(self.app, self.window), - }) - } -} - -impl<'a> Borrow for ElementContext<'a> { - fn borrow(&self) -> &AppContext { - self.cx.app - } -} - -impl<'a> BorrowMut for ElementContext<'a> { - fn borrow_mut(&mut self) -> &mut AppContext { - self.cx.borrow_mut() - } -} - -impl<'a> Borrow> for ElementContext<'a> { - fn borrow(&self) -> &WindowContext<'a> { - &self.cx - } -} - -impl<'a> BorrowMut> for ElementContext<'a> { - fn borrow_mut(&mut self) -> &mut WindowContext<'a> { - &mut self.cx - } -} - -impl<'a> Borrow for ElementContext<'a> { - fn borrow(&self) -> &Window { - self.cx.window - } -} - -impl<'a> BorrowMut for ElementContext<'a> { - fn borrow_mut(&mut self) -> &mut Window { - self.cx.borrow_mut() - } -} - -impl<'a> Context for ElementContext<'a> { - type Result = as Context>::Result; - - fn new_model( - &mut self, - build_model: impl FnOnce(&mut crate::ModelContext<'_, T>) -> T, - ) -> Self::Result> { - self.cx.new_model(build_model) - } - - fn reserve_model(&mut self) -> Self::Result> { - self.cx.reserve_model() - } - - fn insert_model( - &mut self, - reservation: crate::Reservation, - build_model: impl FnOnce(&mut crate::ModelContext<'_, T>) -> T, - ) -> Self::Result> { - self.cx.insert_model(reservation, build_model) - } - - fn update_model( - &mut self, - handle: &crate::Model, - update: impl FnOnce(&mut T, &mut crate::ModelContext<'_, T>) -> R, - ) -> Self::Result - where - T: 'static, - { - self.cx.update_model(handle, update) - } - - fn read_model( - &self, - handle: &crate::Model, - read: impl FnOnce(&T, &AppContext) -> R, - ) -> Self::Result - where - T: 'static, - { - self.cx.read_model(handle, read) - } - - fn update_window(&mut self, window: crate::AnyWindowHandle, f: F) -> Result - where - F: FnOnce(crate::AnyView, &mut WindowContext<'_>) -> T, - { - self.cx.update_window(window, f) - } - - fn read_window( - &self, - window: &crate::WindowHandle, - read: impl FnOnce(crate::View, &AppContext) -> R, - ) -> Result - where - T: 'static, - { - self.cx.read_window(window, read) - } -} - -impl<'a> VisualContext for ElementContext<'a> { - fn new_view( - &mut self, - build_view: impl FnOnce(&mut crate::ViewContext<'_, V>) -> V, - ) -> Self::Result> - where - V: 'static + Render, - { - self.cx.new_view(build_view) - } - - fn update_view( - &mut self, - view: &crate::View, - update: impl FnOnce(&mut V, &mut crate::ViewContext<'_, V>) -> R, - ) -> Self::Result { - self.cx.update_view(view, update) - } - - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut crate::ViewContext<'_, V>) -> V, - ) -> Self::Result> - where - V: 'static + Render, - { - self.cx.replace_root_view(build_view) - } - - fn focus_view(&mut self, view: &crate::View) -> Self::Result<()> - where - V: crate::FocusableView, - { - self.cx.focus_view(view) - } - - fn dismiss_view(&mut self, view: &crate::View) -> Self::Result<()> - where - V: crate::ManagedView, - { - self.cx.dismiss_view(view) - } -} - -impl<'a> ElementContext<'a> { - pub(crate) fn draw_roots(&mut self) { - self.window.draw_phase = DrawPhase::Layout; - self.window.tooltip_bounds.take(); - - // Layout all root elements. - let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any(); - root_element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); - - let mut sorted_deferred_draws = - (0..self.window.next_frame.deferred_draws.len()).collect::>(); - sorted_deferred_draws.sort_by_key(|ix| self.window.next_frame.deferred_draws[*ix].priority); - self.prepaint_deferred_draws(&sorted_deferred_draws); - - let mut prompt_element = None; - let mut active_drag_element = None; - let mut tooltip_element = None; - if let Some(prompt) = self.window.prompt.take() { - let mut element = prompt.view.any_view().into_any(); - element.prepaint_as_root(Point::default(), self.window.viewport_size.into(), self); - prompt_element = Some(element); - self.window.prompt = Some(prompt); - } else if let Some(active_drag) = self.app.active_drag.take() { - let mut element = active_drag.view.clone().into_any(); - let offset = self.mouse_position() - active_drag.cursor_offset; - element.prepaint_as_root(offset, AvailableSpace::min_size(), self); - active_drag_element = Some(element); - self.app.active_drag = Some(active_drag); - } else { - tooltip_element = self.prepaint_tooltip(); - } - - self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position); - - // Now actually paint the elements. - self.window.draw_phase = DrawPhase::Paint; - root_element.paint(self); - - self.paint_deferred_draws(&sorted_deferred_draws); - - if let Some(mut prompt_element) = prompt_element { - prompt_element.paint(self) - } else if let Some(mut drag_element) = active_drag_element { - drag_element.paint(self); - } else if let Some(mut tooltip_element) = tooltip_element { - tooltip_element.paint(self); - } - } - - fn prepaint_tooltip(&mut self) -> Option { - let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?; - let tooltip_request = tooltip_request.unwrap(); - let mut element = tooltip_request.tooltip.view.clone().into_any(); - let mouse_position = tooltip_request.tooltip.mouse_position; - let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); - - let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); - let window_bounds = Bounds { - origin: Point::default(), - size: self.viewport_size(), - }; - - if tooltip_bounds.right() > window_bounds.right() { - let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); - if new_x >= Pixels::ZERO { - tooltip_bounds.origin.x = new_x; - } else { - tooltip_bounds.origin.x = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), - ); - } - } - - if tooltip_bounds.bottom() > window_bounds.bottom() { - let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); - if new_y >= Pixels::ZERO { - tooltip_bounds.origin.y = new_y; - } else { - tooltip_bounds.origin.y = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), - ); - } - } - - self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); - - self.window.tooltip_bounds = Some(TooltipBounds { - id: tooltip_request.id, - bounds: tooltip_bounds, - }); - Some(element) - } - - fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { - assert_eq!(self.window.element_id_stack.len(), 0); - - let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); - for deferred_draw_ix in deferred_draw_indices { - let deferred_draw = &mut deferred_draws[*deferred_draw_ix]; - self.window.element_id_stack = deferred_draw.element_id_stack.clone(); - self.window.text_style_stack = deferred_draw.text_style_stack.clone(); - self.window - .next_frame - .dispatch_tree - .set_active_node(deferred_draw.parent_node); - - let prepaint_start = self.prepaint_index(); - if let Some(element) = deferred_draw.element.as_mut() { - self.with_absolute_element_offset(deferred_draw.absolute_offset, |cx| { - element.prepaint(cx) - }); - } else { - self.reuse_prepaint(deferred_draw.prepaint_range.clone()); - } - let prepaint_end = self.prepaint_index(); - deferred_draw.prepaint_range = prepaint_start..prepaint_end; - } - assert_eq!( - self.window.next_frame.deferred_draws.len(), - 0, - "cannot call defer_draw during deferred drawing" - ); - self.window.next_frame.deferred_draws = deferred_draws; - self.window.element_id_stack.clear(); - self.window.text_style_stack.clear(); - } - - fn paint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) { - assert_eq!(self.window.element_id_stack.len(), 0); - - let mut deferred_draws = mem::take(&mut self.window.next_frame.deferred_draws); - for deferred_draw_ix in deferred_draw_indices { - let mut deferred_draw = &mut deferred_draws[*deferred_draw_ix]; - self.window.element_id_stack = deferred_draw.element_id_stack.clone(); - self.window - .next_frame - .dispatch_tree - .set_active_node(deferred_draw.parent_node); - - let paint_start = self.paint_index(); - if let Some(element) = deferred_draw.element.as_mut() { - element.paint(self); - } else { - self.reuse_paint(deferred_draw.paint_range.clone()); - } - let paint_end = self.paint_index(); - deferred_draw.paint_range = paint_start..paint_end; - } - self.window.next_frame.deferred_draws = deferred_draws; - self.window.element_id_stack.clear(); - } - - pub(crate) fn prepaint_index(&self) -> PrepaintStateIndex { - PrepaintStateIndex { - hitboxes_index: self.window.next_frame.hitboxes.len(), - tooltips_index: self.window.next_frame.tooltip_requests.len(), - deferred_draws_index: self.window.next_frame.deferred_draws.len(), - dispatch_tree_index: self.window.next_frame.dispatch_tree.len(), - accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), - line_layout_index: self.window.text_system.layout_index(), - } - } - - pub(crate) fn reuse_prepaint(&mut self, range: Range) { - let window = &mut self.window; - window.next_frame.hitboxes.extend( - window.rendered_frame.hitboxes[range.start.hitboxes_index..range.end.hitboxes_index] - .iter() - .cloned(), - ); - window.next_frame.tooltip_requests.extend( - window.rendered_frame.tooltip_requests - [range.start.tooltips_index..range.end.tooltips_index] - .iter_mut() - .map(|request| request.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() - .cloned(), - ); - window - .text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - - let reused_subtree = window.next_frame.dispatch_tree.reuse_subtree( - range.start.dispatch_tree_index..range.end.dispatch_tree_index, - &mut window.rendered_frame.dispatch_tree, - ); - window.next_frame.deferred_draws.extend( - window.rendered_frame.deferred_draws - [range.start.deferred_draws_index..range.end.deferred_draws_index] - .iter() - .map(|deferred_draw| DeferredDraw { - parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), - element_id_stack: deferred_draw.element_id_stack.clone(), - text_style_stack: deferred_draw.text_style_stack.clone(), - priority: deferred_draw.priority, - element: None, - absolute_offset: deferred_draw.absolute_offset, - prepaint_range: deferred_draw.prepaint_range.clone(), - paint_range: deferred_draw.paint_range.clone(), - }), - ); - } - - pub(crate) fn paint_index(&self) -> PaintIndex { - PaintIndex { - scene_index: self.window.next_frame.scene.len(), - mouse_listeners_index: self.window.next_frame.mouse_listeners.len(), - input_handlers_index: self.window.next_frame.input_handlers.len(), - cursor_styles_index: self.window.next_frame.cursor_styles.len(), - accessed_element_states_index: self.window.next_frame.accessed_element_states.len(), - line_layout_index: self.window.text_system.layout_index(), - } - } - - pub(crate) fn reuse_paint(&mut self, range: Range) { - let window = &mut self.cx.window; - - window.next_frame.cursor_styles.extend( - window.rendered_frame.cursor_styles - [range.start.cursor_styles_index..range.end.cursor_styles_index] - .iter() - .cloned(), - ); - window.next_frame.input_handlers.extend( - window.rendered_frame.input_handlers - [range.start.input_handlers_index..range.end.input_handlers_index] - .iter_mut() - .map(|handler| handler.take()), - ); - window.next_frame.mouse_listeners.extend( - window.rendered_frame.mouse_listeners - [range.start.mouse_listeners_index..range.end.mouse_listeners_index] - .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() - .cloned(), - ); - window - .text_system - .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); - window.next_frame.scene.replay( - range.start.scene_index..range.end.scene_index, - &window.rendered_frame.scene, - ); - } - - /// Push a text style onto the stack, and call a function with that style active. - /// Use [`AppContext::text_style`] to get the current, combined text style. - pub fn with_text_style(&mut self, style: Option, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - if let Some(style) = style { - self.window.text_style_stack.push(style); - let result = f(self); - self.window.text_style_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the cursor style at the platform level. - pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) { - self.window - .next_frame - .cursor_styles - .push(CursorStyleRequest { - hitbox_id: hitbox.id, - style, - }); - } - - /// Sets a tooltip to be rendered for the upcoming frame - pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId { - let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0)); - self.window - .next_frame - .tooltip_requests - .push(Some(TooltipRequest { id, tooltip })); - id - } - - /// Pushes the given element id onto the global stack and invokes the given closure - /// with a `GlobalElementId`, which disambiguates the given id in the context of its ancestor - /// ids. Because elements are discarded and recreated on each frame, the `GlobalElementId` is - /// used to associate state with identified elements across separate frames. - pub fn with_element_id( - &mut self, - id: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(id) = id.map(Into::into) { - let window = self.window_mut(); - window.element_id_stack.push(id); - let result = f(self); - let window: &mut Window = self.borrow_mut(); - window.element_id_stack.pop(); - result - } else { - f(self) - } - } - - /// Invoke the given function with the given content mask after intersecting it - /// with the current mask. - pub fn with_content_mask( - &mut self, - mask: Option>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(mask) = mask { - let mask = mask.intersect(&self.content_mask()); - self.window_mut().content_mask_stack.push(mask); - let result = f(self); - self.window_mut().content_mask_stack.pop(); - result - } else { - f(self) - } - } - - /// Updates the global element offset relative to the current offset. This is used to implement - /// scrolling. - pub fn with_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if offset.is_zero() { - return f(self); - }; - - let abs_offset = self.element_offset() + offset; - self.with_absolute_element_offset(abs_offset, f) - } - - /// Updates the global element offset based on the given offset. This is used to implement - /// drag handles and other manual painting of elements. - pub fn with_absolute_element_offset( - &mut self, - offset: Point, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - self.window_mut().element_offset_stack.push(offset); - let result = f(self); - self.window_mut().element_offset_stack.pop(); - result - } - - /// Perform prepaint on child elements in a "retryable" manner, so that any side effects - /// of prepaints can be discarded before prepainting again. This is used to support autoscroll - /// where we need to prepaint children to detect the autoscroll bounds, then adjust the - /// element offset and prepaint again. See [`List`] for an example. - pub fn transact(&mut self, f: impl FnOnce(&mut Self) -> Result) -> Result { - let index = self.prepaint_index(); - let result = f(self); - if result.is_err() { - self.window - .next_frame - .hitboxes - .truncate(index.hitboxes_index); - self.window - .next_frame - .tooltip_requests - .truncate(index.tooltips_index); - self.window - .next_frame - .deferred_draws - .truncate(index.deferred_draws_index); - self.window - .next_frame - .dispatch_tree - .truncate(index.dispatch_tree_index); - self.window - .next_frame - .accessed_element_states - .truncate(index.accessed_element_states_index); - self.window - .text_system - .truncate_layouts(index.line_layout_index); - } - result - } - - /// When you call this method during [`prepaint`], containing elements will attempt to - /// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call - /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element - /// that supports this method being called on the elements it contains. - pub fn request_autoscroll(&mut self, bounds: Bounds) { - self.window.requested_autoscroll = Some(bounds); - } - - /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior - /// described in [`request_autoscroll`]. - pub fn take_autoscroll(&mut self) -> Option> { - self.window.requested_autoscroll.take() - } - - /// Remove an asset from GPUI's cache - pub fn remove_cached_asset( - &mut self, - source: &A::Source, - ) -> Option { - self.asset_cache.remove::(source) - } - - /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. - /// Your view will be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call. - /// The results of that call will be cached, and returned on subsequent uses of this API. - /// - /// Use [Self::remove_cached_asset] to reload your asset. - pub fn use_cached_asset( - &mut self, - source: &A::Source, - ) -> Option { - self.asset_cache.get::(source).or_else(|| { - if let Some(asset) = self.use_asset::(source) { - self.asset_cache - .insert::(source.to_owned(), asset.clone()); - Some(asset) - } else { - None - } - }) - } - - /// Asynchronously load an asset, if the asset hasn't finished loading this will return None. - /// Your view will be re-drawn once the asset has finished loading. - /// - /// Note that the multiple calls to this method will only result in one `Asset::load` call at a - /// time. - /// - /// This asset will not be cached by default, see [Self::use_cached_asset] - pub fn use_asset(&mut self, source: &A::Source) -> Option { - let asset_id = (TypeId::of::(), hash(source)); - let mut is_first = false; - let task = self - .loading_assets - .remove(&asset_id) - .map(|boxed_task| *boxed_task.downcast::>>().unwrap()) - .unwrap_or_else(|| { - is_first = true; - let future = A::load(source.clone(), self); - let task = self.background_executor().spawn(future).shared(); - task - }); - - task.clone().now_or_never().or_else(|| { - if is_first { - let parent_id = self.parent_view_id(); - self.spawn({ - let task = task.clone(); - |mut cx| async move { - task.await; - - cx.on_next_frame(move |cx| { - if let Some(parent_id) = parent_id { - cx.notify(parent_id) - } else { - cx.refresh() - } - }); - } - }) - .detach(); - } - - self.loading_assets.insert(asset_id, Box::new(task)); - - None - }) - } - - /// Obtain the current element offset. - pub fn element_offset(&self) -> Point { - self.window() - .element_offset_stack - .last() - .copied() - .unwrap_or_default() - } - - /// Obtain the current content mask. - pub fn content_mask(&self) -> ContentMask { - self.window() - .content_mask_stack - .last() - .cloned() - .unwrap_or_else(|| ContentMask { - bounds: Bounds { - origin: Point::default(), - size: self.window().viewport_size, - }, - }) - } - - /// The size of an em for the base font of the application. Adjusting this value allows the - /// UI to scale, just like zooming a web page. - pub fn rem_size(&self) -> Pixels { - self.window().rem_size - } - - /// Updates or initializes state for an element with the given id that lives across multiple - /// frames. If an element with this ID existed in the rendered frame, its state will be passed - /// to the given closure. The state returned by the closure will be stored so it can be referenced - /// when drawing the next frame. - pub fn with_element_state( - &mut self, - element_id: Option, - f: impl FnOnce(Option>, &mut Self) -> (R, Option), - ) -> R - where - S: 'static, - { - let id_is_none = element_id.is_none(); - self.with_element_id(element_id, |cx| { - if id_is_none { - let (result, state) = f(None, cx); - debug_assert!(state.is_none(), "you must not return an element state when passing None for the element id"); - result - } else { - let global_id = cx.window().element_id_stack.clone(); - let key = (global_id, TypeId::of::()); - cx.window.next_frame.accessed_element_states.push(key.clone()); - - if let Some(any) = cx - .window_mut() - .next_frame - .element_states - .remove(&key) - .or_else(|| { - cx.window_mut() - .rendered_frame - .element_states - .remove(&key) - }) - { - let ElementStateBox { - inner, - #[cfg(debug_assertions)] - type_name - } = any; - // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = inner - .downcast::>() - .map_err(|_| { - #[cfg(debug_assertions)] - { - anyhow::anyhow!( - "invalid element state type for id, requested_type {:?}, actual type: {:?}", - std::any::type_name::(), - type_name - ) - } - - #[cfg(not(debug_assertions))] - { - anyhow::anyhow!( - "invalid element state type for id, requested_type {:?}", - std::any::type_name::(), - ) - } - }) - .unwrap(); - - // Actual: Option <- View - // Requested: () <- AnyElement - let state = state_box - .take() - .expect("reentrant call to with_element_state for the same state type and element id"); - let (result, state) = f(Some(Some(state)), cx); - state_box.replace(state.expect("you must return ")); - cx.window_mut() - .next_frame - .element_states - .insert(key, ElementStateBox { - inner: state_box, - #[cfg(debug_assertions)] - type_name - }); - result - } else { - let (result, state) = f(Some(None), cx); - cx.window_mut() - .next_frame - .element_states - .insert(key, - ElementStateBox { - inner: Box::new(Some(state.expect("you must return Some when you pass some element id"))), - #[cfg(debug_assertions)] - type_name: std::any::type_name::() - } - - ); - result - } - } - }) - } - - /// Defers the drawing of the given element, scheduling it to be painted on top of the currently-drawn tree - /// at a later time. The `priority` parameter determines the drawing order relative to other deferred elements, - /// with higher values being drawn on top. - pub fn defer_draw( - &mut self, - element: AnyElement, - absolute_offset: Point, - priority: usize, - ) { - let window = &mut self.cx.window; - assert_eq!( - window.draw_phase, - DrawPhase::Layout, - "defer_draw can only be called during request_layout or prepaint" - ); - let parent_node = window.next_frame.dispatch_tree.active_node_id().unwrap(); - window.next_frame.deferred_draws.push(DeferredDraw { - parent_node, - element_id_stack: window.element_id_stack.clone(), - text_style_stack: window.text_style_stack.clone(), - priority, - element: Some(element), - absolute_offset, - prepaint_range: PrepaintStateIndex::default()..PrepaintStateIndex::default(), - paint_range: PaintIndex::default()..PaintIndex::default(), - }); - } - - /// Creates a new painting layer for the specified bounds. A "layer" is a batch - /// of geometry that are non-overlapping and have the same draw order. This is typically used - /// for performance reasons. - pub fn paint_layer(&mut self, bounds: Bounds, f: impl FnOnce(&mut Self) -> R) -> R { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - let clipped_bounds = bounds.intersect(&content_mask.bounds); - if !clipped_bounds.is_empty() { - self.window - .next_frame - .scene - .push_layer(clipped_bounds.scale(scale_factor)); - } - - let result = f(self); - - if !clipped_bounds.is_empty() { - self.window.next_frame.scene.pop_layer(); - } - - result - } - - /// Paint one or more drop shadows into the scene for the next frame at the current z-index. - pub fn paint_shadows( - &mut self, - bounds: Bounds, - corner_radii: Corners, - shadows: &[BoxShadow], - ) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - for shadow in shadows { - let mut shadow_bounds = bounds; - shadow_bounds.origin += shadow.offset; - shadow_bounds.dilate(shadow.spread_radius); - self.window.next_frame.scene.insert_primitive(Shadow { - order: 0, - blur_radius: shadow.blur_radius.scale(scale_factor), - bounds: shadow_bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - corner_radii: corner_radii.scale(scale_factor), - color: shadow.color, - }); - } - } - - /// Paint one or more quads into the scene for the next frame at the current stacking context. - /// Quads are colored rectangular regions with an optional background, border, and corner radius. - /// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type. - pub fn paint_quad(&mut self, quad: PaintQuad) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - self.window.next_frame.scene.insert_primitive(Quad { - order: 0, - pad: 0, - bounds: quad.bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - background: quad.background, - border_color: quad.border_color, - corner_radii: quad.corner_radii.scale(scale_factor), - border_widths: quad.border_widths.scale(scale_factor), - }); - } - - /// Paint the given `Path` into the scene for the next frame at the current z-index. - pub fn paint_path(&mut self, mut path: Path, color: impl Into) { - let scale_factor = self.scale_factor(); - let content_mask = self.content_mask(); - path.content_mask = content_mask; - path.color = color.into(); - self.window - .next_frame - .scene - .insert_primitive(path.scale(scale_factor)); - } - - /// Paint an underline into the scene for the next frame at the current z-index. - pub fn paint_underline( - &mut self, - origin: Point, - width: Pixels, - style: &UnderlineStyle, - ) { - let scale_factor = self.scale_factor(); - let height = if style.wavy { - style.thickness * 3. - } else { - style.thickness - }; - let bounds = Bounds { - origin, - size: size(width, height), - }; - let content_mask = self.content_mask(); - - self.window.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds: bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - color: style.color.unwrap_or_default(), - thickness: style.thickness.scale(scale_factor), - wavy: style.wavy, - }); - } - - /// Paint a strikethrough into the scene for the next frame at the current z-index. - pub fn paint_strikethrough( - &mut self, - origin: Point, - width: Pixels, - style: &StrikethroughStyle, - ) { - let scale_factor = self.scale_factor(); - let height = style.thickness; - let bounds = Bounds { - origin, - size: size(width, height), - }; - let content_mask = self.content_mask(); - - self.window.next_frame.scene.insert_primitive(Underline { - order: 0, - pad: 0, - bounds: bounds.scale(scale_factor), - content_mask: content_mask.scale(scale_factor), - thickness: style.thickness.scale(scale_factor), - color: style.color.unwrap_or_default(), - wavy: false, - }); - } - - /// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single glyph that has already been shaped. - pub fn paint_glyph( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - color: Hsla, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let subpixel_variant = Point { - x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, - y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS as f32).floor() as u8, - }; - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - subpixel_variant, - scale_factor, - is_emoji: false, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok((size, Cow::Owned(bytes))) - })?; - let bounds = Bounds { - origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.content_mask().scale(scale_factor); - self.window - .next_frame - .scene - .insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color, - tile, - transformation: TransformationMatrix::unit(), - }); - } - Ok(()) - } - - /// Paints an emoji glyph into the scene for the next frame at the current z-index. - /// - /// The y component of the origin is the baseline of the glyph. - /// You should generally prefer to use the [`ShapedLine::paint`](crate::ShapedLine::paint) or - /// [`WrappedLine::paint`](crate::WrappedLine::paint) methods in the [`TextSystem`](crate::TextSystem). - /// This method is only useful if you need to paint a single emoji that has already been shaped. - pub fn paint_emoji( - &mut self, - origin: Point, - font_id: FontId, - glyph_id: GlyphId, - font_size: Pixels, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let glyph_origin = origin.scale(scale_factor); - let params = RenderGlyphParams { - font_id, - glyph_id, - font_size, - // We don't render emojis with subpixel variants. - subpixel_variant: Default::default(), - scale_factor, - is_emoji: true, - }; - - let raster_bounds = self.text_system().raster_bounds(¶ms)?; - if !raster_bounds.is_zero() { - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?; - Ok((size, Cow::Owned(bytes))) - })?; - let bounds = Bounds { - origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into), - size: tile.bounds.size.map(Into::into), - }; - let content_mask = self.content_mask().scale(scale_factor); - - self.window - .next_frame - .scene - .insert_primitive(PolychromeSprite { - order: 0, - grayscale: false, - bounds, - corner_radii: Default::default(), - content_mask, - tile, - }); - } - Ok(()) - } - - /// Paint a monochrome SVG into the scene for the next frame at the current stacking context. - pub fn paint_svg( - &mut self, - bounds: Bounds, - path: SharedString, - transformation: TransformationMatrix, - color: Hsla, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - // Render the SVG at twice the size to get a higher quality result. - let params = RenderSvgParams { - path, - size: bounds - .size - .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)), - }; - - let tile = - self.window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - let bytes = self.svg_renderer.render(¶ms)?; - Ok((params.size, Cow::Owned(bytes))) - })?; - let content_mask = self.content_mask().scale(scale_factor); - - self.window - .next_frame - .scene - .insert_primitive(MonochromeSprite { - order: 0, - pad: 0, - bounds, - content_mask, - color, - tile, - transformation, - }); - - Ok(()) - } - - /// Paint an image into the scene for the next frame at the current z-index. - pub fn paint_image( - &mut self, - bounds: Bounds, - corner_radii: Corners, - data: Arc, - grayscale: bool, - ) -> Result<()> { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - let params = RenderImageParams { image_id: data.id }; - - let tile = self - .window - .sprite_atlas - .get_or_insert_with(¶ms.clone().into(), &mut || { - Ok((data.size(), Cow::Borrowed(data.as_bytes()))) - })?; - let content_mask = self.content_mask().scale(scale_factor); - let corner_radii = corner_radii.scale(scale_factor); - - self.window - .next_frame - .scene - .insert_primitive(PolychromeSprite { - order: 0, - grayscale, - bounds, - content_mask, - corner_radii, - tile, - }); - Ok(()) - } - - /// Paint a surface into the scene for the next frame at the current z-index. - #[cfg(target_os = "macos")] - pub fn paint_surface(&mut self, bounds: Bounds, image_buffer: CVImageBuffer) { - let scale_factor = self.scale_factor(); - let bounds = bounds.scale(scale_factor); - let content_mask = self.content_mask().scale(scale_factor); - self.window - .next_frame - .scene - .insert_primitive(crate::Surface { - order: 0, - bounds, - content_mask, - image_buffer, - }); - } - - #[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 - /// calls to the `Element::layout` trait method and enables any element to participate in layout. - pub fn request_layout( - &mut self, - style: &Style, - children: impl IntoIterator, - ) -> LayoutId { - self.app.layout_id_buffer.clear(); - self.app.layout_id_buffer.extend(children); - let rem_size = self.rem_size(); - - self.cx - .window - .layout_engine - .as_mut() - .unwrap() - .request_layout(style, rem_size, &self.cx.app.layout_id_buffer) - } - - /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, - /// this variant takes a function that is invoked during layout so you can use arbitrary logic to - /// determine the element's size. One place this is used internally is when measuring text. - /// - /// The given closure is invoked at layout time with the known dimensions and available space and - /// returns a `Size`. - pub fn request_measured_layout< - F: FnMut(Size>, Size, &mut WindowContext) -> Size - + 'static, - >( - &mut self, - style: Style, - measure: F, - ) -> LayoutId { - let rem_size = self.rem_size(); - self.window - .layout_engine - .as_mut() - .unwrap() - .request_measured_layout(style, rem_size, measure) - } - - /// Compute the layout for the given id within the given available space. - /// This method is called for its side effect, typically by the framework prior to painting. - /// After calling it, you can request the bounds of the given layout node id or any descendant. - pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { - let mut layout_engine = self.window.layout_engine.take().unwrap(); - layout_engine.compute_layout(layout_id, available_space, self); - self.window.layout_engine = Some(layout_engine); - } - - /// Obtain the bounds computed for the given LayoutId relative to the window. This method will usually be invoked by - /// GPUI itself automatically in order to pass your element its `Bounds` automatically. - pub fn layout_bounds(&mut self, layout_id: LayoutId) -> Bounds { - let mut bounds = self - .window - .layout_engine - .as_mut() - .unwrap() - .layout_bounds(layout_id) - .map(Into::into); - bounds.origin += self.element_offset(); - bounds - } - - /// This method should be called during `prepaint`. You can use - /// the returned [Hitbox] during `paint` or in an event handler - /// to determine whether the inserted hitbox was the topmost. - pub fn insert_hitbox(&mut self, bounds: Bounds, opaque: bool) -> Hitbox { - let content_mask = self.content_mask(); - let window = &mut self.window; - let id = window.next_hitbox_id; - window.next_hitbox_id.0 += 1; - let hitbox = Hitbox { - id, - bounds, - content_mask, - opaque, - }; - window.next_frame.hitboxes.push(hitbox.clone()); - hitbox - } - - /// Sets the key context for the current element. This context will be used to translate - /// keybindings into actions. - pub fn set_key_context(&mut self, context: KeyContext) { - self.window - .next_frame - .dispatch_tree - .set_key_context(context); - } - - /// Sets the focus handle for the current element. This handle will be used to manage focus state - /// and keyboard event dispatch for the element. - pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) { - self.window - .next_frame - .dispatch_tree - .set_focus_id(focus_handle.id); - } - - /// Sets the view id for the current element, which will be used to manage view caching. - pub fn set_view_id(&mut self, view_id: EntityId) { - self.window.next_frame.dispatch_tree.set_view_id(view_id); - } - - /// Get the last view id for the current element - pub fn parent_view_id(&mut self) -> Option { - self.window.next_frame.dispatch_tree.parent_view_id() - } - - /// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the - /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. This handler will be active for the upcoming frame until the following frame is - /// rendered. - /// - /// [element_input_handler]: crate::ElementInputHandler - pub fn handle_input(&mut self, focus_handle: &FocusHandle, input_handler: impl InputHandler) { - if focus_handle.is_focused(self) { - let cx = self.to_async(); - self.window - .next_frame - .input_handlers - .push(Some(PlatformInputHandler::new(cx, Box::new(input_handler)))); - } - } - - /// Register a mouse event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - pub fn on_mouse_event( - &mut self, - mut handler: impl FnMut(&Event, DispatchPhase, &mut ElementContext) + 'static, - ) { - self.window.next_frame.mouse_listeners.push(Some(Box::new( - move |event: &dyn Any, phase: DispatchPhase, cx: &mut ElementContext<'_>| { - if let Some(event) = event.downcast_ref() { - handler(event, phase, cx) - } - }, - ))); - } - - /// Register a key event listener on the window for the next frame. The type of event - /// is determined by the first parameter of the given listener. When the next frame is rendered - /// the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a global listener. - pub fn on_key_event( - &mut self, - listener: impl Fn(&Event, DispatchPhase, &mut ElementContext) + 'static, - ) { - self.window.next_frame.dispatch_tree.on_key_event(Rc::new( - move |event: &dyn Any, phase, cx: &mut ElementContext<'_>| { - if let Some(event) = event.downcast_ref::() { - listener(event, phase, cx) - } - }, - )); - } - - /// Register a modifiers changed event listener on the window for the next frame. - /// - /// This is a fairly low-level method, so prefer using event handlers on elements unless you have - /// a specific need to register a global listener. - pub fn on_modifiers_changed( - &mut self, - listener: impl Fn(&ModifiersChangedEvent, &mut ElementContext) + 'static, - ) { - self.window - .next_frame - .dispatch_tree - .on_modifiers_changed(Rc::new( - move |event: &ModifiersChangedEvent, cx: &mut ElementContext<'_>| { - listener(event, cx) - }, - )); - } -} diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 62ed376463..ec7cf222ba 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -155,7 +155,7 @@ impl FocusableView for ImageView { impl Render for ImageView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let checkered_background = |bounds: Bounds, _, cx: &mut ElementContext| { + let checkered_background = |bounds: Bounds, _, cx: &mut WindowContext| { let square_size = 32.0; let start_y = bounds.origin.y.0; diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 7547c9603f..909ab2a6f1 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,11 +1,11 @@ use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ - div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, ElementContext, - FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hitbox, Hsla, InputHandler, - InteractiveElement, Interactivity, IntoElement, LayoutId, Model, ModelContext, - ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine, - StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UnderlineStyle, - WeakView, WhiteSpace, WindowContext, WindowTextSystem, + div, fill, point, px, relative, AnyElement, Bounds, DispatchPhase, Element, FocusHandle, Font, + FontStyle, FontWeight, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, + Interactivity, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, + MouseMoveEvent, Pixels, Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, + Styled, TextRun, TextStyle, UnderlineStyle, WeakView, WhiteSpace, WindowContext, + WindowTextSystem, }; use itertools::Itertools; use language::CursorShape; @@ -85,7 +85,7 @@ impl LayoutCell { origin: Point, layout: &LayoutState, _visible_bounds: Bounds, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let pos = { let point = self.point; @@ -124,7 +124,7 @@ impl LayoutRect { } } - fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut ElementContext) { + fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut WindowContext) { let position = { let alac_point = self.point; point( @@ -418,7 +418,7 @@ impl TerminalElement { origin: Point, mode: TermMode, hitbox: &Hitbox, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { let focus = self.focus.clone(); let terminal = self.terminal.clone(); @@ -544,7 +544,7 @@ impl Element for TerminalElement { type RequestLayoutState = (); type PrepaintState = LayoutState; - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { self.interactivity.occlude_mouse(); let layout_id = self.interactivity.request_layout(cx, |mut style, cx| { style.size.width = relative(1.).into(); @@ -560,7 +560,7 @@ impl Element for TerminalElement { &mut self, bounds: Bounds, _: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Self::PrepaintState { self.interactivity .prepaint(bounds, bounds.size, cx, |_, _, hitbox, cx| { @@ -777,7 +777,7 @@ impl Element for TerminalElement { bounds: Bounds, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, - cx: &mut ElementContext<'_>, + cx: &mut WindowContext<'_>, ) { cx.paint_quad(fill(bounds, layout.background_color)); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index a3a9eba8f2..0d842d2a03 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -2,9 +2,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ anchored, deferred, div, point, prelude::FluentBuilder, px, AnchorCorner, AnyElement, Bounds, - DismissEvent, DispatchPhase, Element, ElementContext, ElementId, HitboxId, InteractiveElement, - IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, - VisualContext, WindowContext, + DismissEvent, DispatchPhase, Element, ElementId, HitboxId, InteractiveElement, IntoElement, + LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, + WindowContext, }; use crate::prelude::*; @@ -112,8 +112,8 @@ impl PopoverMenu { fn with_element_state( &mut self, - cx: &mut ElementContext, - f: impl FnOnce(&mut Self, &mut PopoverMenuElementState, &mut ElementContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&mut Self, &mut PopoverMenuElementState, &mut WindowContext) -> R, ) -> R { cx.with_element_state::, _>( Some(self.id.clone()), @@ -173,7 +173,7 @@ impl Element for PopoverMenu { fn request_layout( &mut self, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { self.with_element_state(cx, |this, element_state, cx| { let mut menu_layout_id = None; @@ -221,7 +221,7 @@ impl Element for PopoverMenu { &mut self, _bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> Option { self.with_element_state(cx, |_this, element_state, cx| { if let Some(child) = request_layout.child_element.as_mut() { @@ -245,7 +245,7 @@ impl Element for PopoverMenu { _: Bounds, request_layout: &mut Self::RequestLayoutState, child_hitbox: &mut Option, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { self.with_element_state(cx, |_this, _element_state, cx| { if let Some(mut child) = request_layout.child_element.take() { diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 5382236271..e656835c1d 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -2,9 +2,8 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ anchored, deferred, div, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, - Element, ElementContext, ElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, - ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, - WindowContext, + Element, ElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, ManagedView, + MouseButton, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; pub struct RightClickMenu { @@ -41,8 +40,8 @@ impl RightClickMenu { fn with_element_state( &mut self, - cx: &mut ElementContext, - f: impl FnOnce(&mut Self, &mut MenuHandleElementState, &mut ElementContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&mut Self, &mut MenuHandleElementState, &mut WindowContext) -> R, ) -> R { cx.with_element_state::, _>( Some(self.id.clone()), @@ -89,19 +88,24 @@ impl Default for MenuHandleElementState { } } -pub struct MenuHandleFrameState { +pub struct RequestLayoutState { child_layout_id: Option, child_element: Option, menu_element: Option, } +pub struct PrepaintState { + hitbox: Hitbox, + child_bounds: Option>, +} + impl Element for RightClickMenu { - type RequestLayoutState = MenuHandleFrameState; - type PrepaintState = Hitbox; + type RequestLayoutState = RequestLayoutState; + type PrepaintState = PrepaintState; fn request_layout( &mut self, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { self.with_element_state(cx, |this, element_state, cx| { let mut menu_layout_id = None; @@ -137,7 +141,7 @@ impl Element for RightClickMenu { ( layout_id, - MenuHandleFrameState { + RequestLayoutState { child_element, child_layout_id, menu_element, @@ -150,8 +154,8 @@ impl Element for RightClickMenu { &mut self, bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - cx: &mut ElementContext, - ) -> Hitbox { + cx: &mut WindowContext, + ) -> PrepaintState { cx.with_element_id(Some(self.id.clone()), |cx| { let hitbox = cx.insert_hitbox(bounds, false); @@ -163,7 +167,12 @@ impl Element for RightClickMenu { menu.prepaint(cx); } - hitbox + PrepaintState { + hitbox, + child_bounds: request_layout + .child_layout_id + .map(|layout_id| cx.layout_bounds(layout_id)), + } }) } @@ -171,8 +180,8 @@ impl Element for RightClickMenu { &mut self, _bounds: Bounds, request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, - cx: &mut ElementContext, + prepaint_state: &mut Self::PrepaintState, + cx: &mut WindowContext, ) { self.with_element_state(cx, |this, element_state, cx| { if let Some(mut child) = request_layout.child_element.take() { @@ -191,10 +200,9 @@ impl Element for RightClickMenu { let attach = this.attach; let menu = element_state.menu.clone(); let position = element_state.position.clone(); - let child_layout_id = request_layout.child_layout_id; - let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); + let child_bounds = prepaint_state.child_bounds; - let hitbox_id = hitbox.id; + let hitbox_id = prepaint_state.hitbox.id; cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && event.button == MouseButton::Right @@ -219,7 +227,7 @@ impl Element for RightClickMenu { .detach(); cx.focus_view(&new_menu); *menu.borrow_mut() = Some(new_menu); - *position.borrow_mut() = if child_layout_id.is_some() { + *position.borrow_mut() = if let Some(child_bounds) = child_bounds { if let Some(attach) = attach { attach.corner(child_bounds) } else { diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 5cf56bc58d..9a0f0ed1d2 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -2,9 +2,9 @@ pub use gpui::prelude::*; pub use gpui::{ - div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementContext, - ElementId, InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, - ViewContext, WindowContext, + div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId, + InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, ViewContext, + WindowContext, }; pub use crate::clickable::*; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index faf25457d9..9ad7acf734 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -759,7 +759,7 @@ mod element { fn layout_handle( axis: Axis, pane_bounds: Bounds, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> PaneAxisHandleLayout { let handle_bounds = Bounds { origin: pane_bounds.origin.apply_along(axis, |origin| { @@ -797,7 +797,7 @@ mod element { fn request_layout( &mut self, - cx: &mut ui::prelude::ElementContext, + cx: &mut ui::prelude::WindowContext, ) -> (gpui::LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.flex_grow = 1.; @@ -812,7 +812,7 @@ mod element { &mut self, bounds: Bounds, _state: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) -> PaneAxisLayout { let dragged_handle = cx.with_element_state::>>, _>( Some(self.basis.into()), @@ -900,7 +900,7 @@ mod element { bounds: gpui::Bounds, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, - cx: &mut ui::prelude::ElementContext, + cx: &mut ui::prelude::WindowContext, ) { for child in &mut layout.children { child.element.paint(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 94890bc15c..8e29ce22e0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -78,9 +78,9 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use ui::{ - div, h_flex, Context as _, Div, Element, ElementContext, FluentBuilder, - InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString, - Styled as _, ViewContext, VisualContext as _, WindowContext, + div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement, + Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, + WindowContext, }; use util::{maybe, ResultExt}; use uuid::Uuid; @@ -4991,7 +4991,7 @@ impl Element for DisconnectedOverlay { type RequestLayoutState = AnyElement; type PrepaintState = (); - fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) { + fn request_layout(&mut self, cx: &mut WindowContext) -> (LayoutId, Self::RequestLayoutState) { let mut background = cx.theme().colors().elevated_surface_background; background.fade_out(0.2); let mut overlay = div() @@ -5016,7 +5016,7 @@ impl Element for DisconnectedOverlay { &mut self, bounds: Bounds, overlay: &mut Self::RequestLayoutState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { cx.insert_hitbox(bounds, true); overlay.prepaint(cx); @@ -5027,7 +5027,7 @@ impl Element for DisconnectedOverlay { _: Bounds, overlay: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, - cx: &mut ElementContext, + cx: &mut WindowContext, ) { overlay.paint(cx) } From bb213b6e37da352a2721d8ffb9c9d66c02b1632b Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 25 Apr 2024 13:44:24 +0200 Subject: [PATCH 055/101] Fix keybinding errors on Linux (#10982) These showed up as error messages. One of them has been removed and the other two have changed names. Release Notes: - N/A --- assets/keymaps/default-linux.json | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 209699b3cd..6fb4647798 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -297,13 +297,8 @@ "ctrl-shift-k": "editor::DeleteLine", "alt-up": "editor::MoveLineUp", "alt-down": "editor::MoveLineDown", - "ctrl-alt-shift-up": [ - "editor::DuplicateLine", - { - "move_upwards": true - } - ], - "ctrl-alt-shift-down": "editor::DuplicateLine", + "ctrl-alt-shift-up": "editor::DuplicateLineUp", + "ctrl-alt-shift-down": "editor::DuplicateLineDown", "ctrl-shift-left": "editor::SelectToPreviousWordStart", "ctrl-shift-right": "editor::SelectToNextWordEnd", "ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding @@ -593,12 +588,6 @@ "tab": "channel_modal::ToggleMode" } }, - { - "context": "ChatPanel > MessageEditor", - "bindings": { - "escape": "chat_panel::CloseReplyPreview" - } - }, { "context": "FileFinder", "bindings": { "ctrl-shift-p": "file_finder::SelectPrev" } From 019821d62c15bae14d010416608effafc4ca7241 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 25 Apr 2024 14:49:07 +0200 Subject: [PATCH 056/101] eslint: register as language server for Vue.js (#10983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes #9934 and does two things: 1. It registers ESLint as a secondary language server for Vue.js files (`.vue`) 2. It registers ESLint as a _secondary_ (instead of primary) language server for TypeScript, JavaScript and TSX. The second point because I don't see any reason why ESLint should be registered as a primary language server for these languages. I read through the code in `project.rs` that uses the primary language server and I don't think there will be any differences to how it previously worked. I also manually tested ESLint support in a Vue.js project, a Next.js project and a plain old JS project — still works in all three. Release Notes: - Added ESLint support for Vue.js files by starting it as a language server on `.vue` files. ([#9934](https://github.com/zed-industries/zed/issues/9934)). --- crates/languages/src/lib.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index a25368f903..ff315727c4 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -193,24 +193,21 @@ pub fn init( ); language!( "tsx", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ] + vec![Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone() + ))] ); language!( "typescript", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ] + vec![Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone() + ))] ); language!( "javascript", - vec![ - Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), - Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), - ] + vec![Arc::new(typescript::TypeScriptLspAdapter::new( + node_runtime.clone() + ))] ); language!( "jsdoc", @@ -250,6 +247,14 @@ pub fn init( ); } + let eslint_languages = ["TSX", "TypeScript", "JavaScript", "Vue.js"]; + for language in eslint_languages { + languages.register_secondary_lsp_adapter( + language.into(), + Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + ); + } + let mut subscription = languages.subscribe(); let mut prev_language_settings = languages.language_settings(); From 7ec963664e64a1e776c521c8e39391c81de9067d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 25 Apr 2024 15:02:19 +0200 Subject: [PATCH 057/101] git blame: Do not try to blame buffer if it has no file (#10985) Release Notes: - Fixed error messages being logged due to inline git blame not working on an empty buffer that hasn't been saved yet. --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a2fc4d67cf..9b2cfdba2b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8982,6 +8982,10 @@ impl Editor { return; }; + if buffer.read(cx).file().is_none() { + return; + } + let project = project.clone(); let blame = cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, cx)); self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify())); From 0de2636324c818ba74f55f8a48ad74b3a2ffe368 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 09:10:02 -0400 Subject: [PATCH 058/101] Revert "Changed cmd+w with no open tabs to close window (#10740)" (#10986) This PR reverts #10740, as it makes it too easy to close Zed accidentally. Quitting Zed when you don't mean to is disruptive and can break your flow. This is even more the case when you're collaborating. Therefore, we shouldn't make it easy to quit Zed when you don't mean to. If we want to bring back this behavior it needs to have a corresponding setting that should, in my opinion, be **off** by default. Additionally, someone made the good point that this behavior should not be present on Linux or Windows. This reverts commit 5102e37a5bc344cb1f5f2a4116b55d4d3afafbb0. Release Notes: - Changed `cmd-w` with no open tabs to no longer close the window (preview-only). --- crates/workspace/src/pane.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ddc81a3e12..c9027f2c90 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -5,7 +5,7 @@ use crate::{ }, toolbar::Toolbar, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, - CloseWindow, NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible, + NewCenterTerminal, NewFile, NewSearch, OpenInTerminal, OpenTerminal, OpenVisible, SplitDirection, ToggleZoom, Workspace, }; use anyhow::Result; @@ -879,8 +879,6 @@ impl Pane { cx: &mut ViewContext, ) -> Option>> { if self.items.is_empty() { - // Close the window when there's no active items to close. - cx.dispatch_action(Box::new(CloseWindow)); return None; } let active_item_id = self.items[self.active_item_index].item_id(); From 530224527d68809becc86191b2a85b89fc1110a2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 25 Apr 2024 15:12:58 +0200 Subject: [PATCH 059/101] Allow pressing `escape` to cancel the current assistant generation (#10987) If the assistant has already emitted some text, we will leave the assistant message but maintain the cursor on the previous user message, so that the user can easily discard the message by submitting again. If no output was emitted yet, we simply delete the empty assistant message. Release Notes: - N/A --- assets/keymaps/default-macos.json | 3 ++- crates/assistant2/src/assistant2.rs | 32 ++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f4da3078ad..4650df181d 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -212,7 +212,8 @@ "context": "AssistantChat > Editor", // Used in the assistant2 crate "bindings": { "enter": ["assistant2::Submit", "Simple"], - "cmd-enter": ["assistant2::Submit", "Codebase"] + "cmd-enter": ["assistant2::Submit", "Codebase"], + "escape": "assistant2::Cancel" } }, { diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 683ce911af..b89291bd13 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -34,8 +34,6 @@ pub use assistant_settings::AssistantSettings; const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5; -// gpui::actions!(assistant, [Submit]); - #[derive(Eq, PartialEq, Copy, Clone, Deserialize)] pub struct Submit(SubmitMode); @@ -50,7 +48,7 @@ pub enum SubmitMode { Codebase, } -gpui::actions!(assistant2, [ToggleFocus]); +gpui::actions!(assistant2, [Cancel, ToggleFocus]); gpui::impl_actions!(assistant2, [Submit]); pub fn init(client: Arc, cx: &mut AppContext) { @@ -256,6 +254,21 @@ impl AssistantChat { }) } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.pending_completion.take().is_none() { + cx.propagate(); + return; + } + + if let Some(ChatMessage::Assistant(message)) = self.messages.last() { + if message.body.text.is_empty() { + self.pop_message(cx); + } else { + self.push_new_user_message(false, cx); + } + } + } + fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext) { let Some(focused_message_id) = self.focused_message_id(cx) else { log::error!("unexpected state: no user message editor is focused."); @@ -282,6 +295,7 @@ impl AssistantChat { .focus_handle(cx) .contains_focused(cx); this.push_new_user_message(focus, cx); + this.pending_completion = None; }) .context("Failed to push new user message") .log_err(); @@ -453,6 +467,17 @@ impl AssistantChat { cx.notify(); } + fn pop_message(&mut self, cx: &mut ViewContext) { + if self.messages.is_empty() { + return; + } + + self.messages.pop(); + self.list_state + .splice(self.messages.len()..self.messages.len() + 1, 0); + cx.notify(); + } + fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext) { if let Some(index) = self.messages.iter().position(|message| match message { ChatMessage::User(message) => message.id == last_message_id, @@ -677,6 +702,7 @@ impl Render for AssistantChat { .flex_1() .v_flex() .key_context("AssistantChat") + .on_action(cx.listener(Self::cancel)) .text_color(Color::Default.color(cx)) .child(self.render_model_dropdown(cx)) .child(list(self.list_state.clone()).flex_1()) From 1cd34fdd9c3a0ba6e03f1ce2a69a95afda68f605 Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Thu, 25 Apr 2024 15:44:52 +0200 Subject: [PATCH 060/101] Recognize `PKGBUILD` as bash script (#10946) [PKGBUILD] is a file used in the build system of arch linux, and it is basically just a bash script with special functions. Release Notes: - Changed `PKGBUILD` files to be recognized as bash. --- crates/languages/src/bash/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/bash/config.toml b/crates/languages/src/bash/config.toml index 47c8f9e28f..1deb800219 100644 --- a/crates/languages/src/bash/config.toml +++ b/crates/languages/src/bash/config.toml @@ -1,7 +1,7 @@ name = "Shell Script" code_fence_block_name = "bash" grammar = "bash" -path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"] +path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env", "PKGBUILD"] line_comments = ["# "] first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" brackets = [ From 11bcfea6d29c8fe0945b6683264230023a037d31 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 25 Apr 2024 17:04:20 +0200 Subject: [PATCH 061/101] Fix single-line editors not working anymore (#10994) This was introduced with #10979 and was caused by a missing call to `cx.set_view_id` in `EditorElement`, which is necessary when rendering `EditorElement` manually instead of via a view. Release Notes: - N/A --- crates/editor/src/element.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 42b6bfb90b..14e5af444a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -23,11 +23,11 @@ use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; 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, Hitbox, - Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, - ShapedLine, SharedString, Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, - TextStyle, TextStyleRefinement, View, ViewContext, WeakView, WindowContext, + ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, + Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, + ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful, StatefulInteractiveElement, Style, + Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -3417,6 +3417,7 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; + cx.set_view_id(self.editor.entity_id()); cx.with_text_style(Some(text_style), |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { let mut snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx)); From 21022f16440cd585b1f98720777a98ce9e706678 Mon Sep 17 00:00:00 2001 From: Congyu <52687642+Congyuwang@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:07:14 +0800 Subject: [PATCH 062/101] Fix cmd+click find all references fallback not working in Vim mode (#10684) Exclude go-to-definition links returned by LSP that points to the current cursor position. This fixes #10392 . Related PR #9243 . The previous implementation first performs go-to-definition, and if the selected text covers the clicked position, it figures out that it is already clicking on a definition, and should instead look for references. However, the selected range does not necessarily cover the clicked position after clicking on a definition, as in VIM mode. After the PR, if cmd+click on definitions, the definition links would be an empty list, so no go-to-definition is performed, and find-all-references is performed instead. Release Notes: - Fixed #10392 , now `cmd+click`ing to find all references works in vim mode. --- crates/editor/src/editor.rs | 8 ++- crates/editor/src/hover_links.rs | 112 +++++++++++++------------------ 2 files changed, 53 insertions(+), 67 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9b2cfdba2b..f51c3237ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7745,7 +7745,13 @@ impl Editor { .update(&mut cx, |editor, cx| { editor.navigate_to_hover_links( Some(kind), - definitions.into_iter().map(HoverLink::Text).collect(), + definitions + .into_iter() + .filter(|location| { + hover_links::exclude_link_to_position(&buffer, &head, location, cx) + }) + .map(HoverLink::Text) + .collect::>(), split, cx, ) diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 83b767e8be..fb6a5c3126 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -3,7 +3,7 @@ use crate::{ Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId, PointForPosition, SelectPhase, }; -use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext}; +use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext}; use language::{Bias, ToOffset}; use linkify::{LinkFinder, LinkKind}; use lsp::LanguageServerId; @@ -11,8 +11,7 @@ use project::{ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, ResolveState, }; -use std::{cmp, ops::Range}; -use text::Point; +use std::ops::Range; use theme::ActiveTheme as _; use util::{maybe, ResultExt, TryFutureExt}; @@ -85,6 +84,25 @@ impl TriggerPoint { } } +pub fn exclude_link_to_position( + buffer: &Model, + current_position: &text::Anchor, + location: &LocationLink, + cx: &AppContext, +) -> bool { + // Exclude definition links that points back to cursor position. + // (i.e., currently cursor upon definition). + let snapshot = buffer.read(cx).snapshot(); + !(buffer == &location.target.buffer + && current_position + .bias_right(&snapshot) + .cmp(&location.target.range.start, &snapshot) + .is_ge() + && current_position + .cmp(&location.target.range.end, &snapshot) + .is_le()) +} + impl Editor { pub(crate) fn update_hovered_link( &mut self, @@ -132,28 +150,12 @@ impl Editor { modifiers: Modifiers, cx: &mut ViewContext, ) { - let selection_before_revealing = self.selections.newest::(cx); - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - let before_revealing_head = selection_before_revealing.head(); - let before_revealing_tail = selection_before_revealing.tail(); - let before_revealing = match before_revealing_tail.cmp(&before_revealing_head) { - cmp::Ordering::Equal | cmp::Ordering::Less => { - multi_buffer_snapshot.anchor_after(before_revealing_head) - ..multi_buffer_snapshot.anchor_before(before_revealing_tail) - } - cmp::Ordering::Greater => { - multi_buffer_snapshot.anchor_before(before_revealing_tail) - ..multi_buffer_snapshot.anchor_after(before_revealing_head) - } - }; - drop(multi_buffer_snapshot); - let reveal_task = self.cmd_click_reveal_task(point, modifiers, cx); cx.spawn(|editor, mut cx| async move { let definition_revealed = reveal_task.await.log_err().unwrap_or(false); let find_references = editor .update(&mut cx, |editor, cx| { - if definition_revealed && revealed_elsewhere(editor, before_revealing, cx) { + if definition_revealed { return None; } editor.find_all_references(&FindAllReferences, cx) @@ -180,12 +182,30 @@ impl Editor { cx.focus(&self.focus_handle); } - return self.navigate_to_hover_links( - None, - hovered_link_state.links, - modifiers.alt, - cx, - ); + // exclude links pointing back to the current anchor + let current_position = point + .next_valid + .to_point(&self.snapshot(cx).display_snapshot); + let Some((buffer, anchor)) = self + .buffer() + .read(cx) + .text_anchor_for_position(current_position, cx) + else { + return Task::ready(Ok(false)); + }; + let links = hovered_link_state + .links + .into_iter() + .filter(|link| { + if let HoverLink::Text(location) = link { + exclude_link_to_position(&buffer, &anchor, location, cx) + } else { + true + } + }) + .collect(); + + return self.navigate_to_hover_links(None, links, modifiers.alt, cx); } } @@ -212,46 +232,6 @@ impl Editor { } } -fn revealed_elsewhere( - editor: &mut Editor, - before_revealing: Range, - cx: &mut ViewContext<'_, Editor>, -) -> bool { - let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - - let selection_after_revealing = editor.selections.newest::(cx); - let after_revealing_head = selection_after_revealing.head(); - let after_revealing_tail = selection_after_revealing.tail(); - let after_revealing = match after_revealing_tail.cmp(&after_revealing_head) { - cmp::Ordering::Equal | cmp::Ordering::Less => { - multi_buffer_snapshot.anchor_after(after_revealing_tail) - ..multi_buffer_snapshot.anchor_before(after_revealing_head) - } - cmp::Ordering::Greater => { - multi_buffer_snapshot.anchor_after(after_revealing_head) - ..multi_buffer_snapshot.anchor_before(after_revealing_tail) - } - }; - - let before_intersects_after_range = (before_revealing - .start - .cmp(&after_revealing.start, &multi_buffer_snapshot) - .is_ge() - && before_revealing - .start - .cmp(&after_revealing.end, &multi_buffer_snapshot) - .is_le()) - || (before_revealing - .end - .cmp(&after_revealing.start, &multi_buffer_snapshot) - .is_ge() - && before_revealing - .end - .cmp(&after_revealing.end, &multi_buffer_snapshot) - .is_le()); - !before_intersects_after_range -} - pub fn update_inlay_link_and_hover_points( snapshot: &EditorSnapshot, point_for_position: PointForPosition, From 3ce4ff94ae225ef548ee8a1c2dff1f761e6e3d45 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 11:17:25 -0400 Subject: [PATCH 063/101] Update `Cargo.lock` --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 34f4720b75..20f15bb965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11095,6 +11095,7 @@ dependencies = [ "futures 0.3.28", "gpui", "indoc", + "itertools 0.11.0", "language", "log", "lsp", From 0d6fb08b67e26f5e6abd14ff51b3a9ba1d89b9c0 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 25 Apr 2024 17:56:53 +0200 Subject: [PATCH 064/101] vim: set cursor to hollow-block if editor loses focus (#10995) This has been bugging me for a while now. Finally figured out how to do it. Release Notes: - Fixed cursor in Vim mode not changing into a hollow block when editor loses focus. Demo: https://github.com/zed-industries/zed/assets/1185253/c7585282-156d-4ab2-b516-eb1940d6d0d3 --- crates/vim/src/editor_events.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 3fccf2eba1..ee5f4cde09 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -51,6 +51,9 @@ fn blurred(editor: View, cx: &mut WindowContext) { vim.clear_operator(cx); } } + editor.update(cx, |editor, cx| { + editor.set_cursor_shape(language::CursorShape::Hollow, cx); + }); }); } From 7065da2b984e7aaa3e5e6e04bdb2c5168985ed66 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 26 Apr 2024 01:01:24 +0800 Subject: [PATCH 065/101] Fix project-panel double click file support on Windows (#10917) Release Notes: - Fixed project panel double click to force open file on Windows. Ref issue: #10898 @bennetbo I saw you was also used `event.down.click_count` in Markdown Preview. https://github.com/zed-industries/zed/commit/7dccbd8e3b61c48ae174807a4023332b7c1df06a#diff-c8d1735cb347ea08d03198df112343ec50a74de8d50414a6f3be6c6d674c6d19R161 And this also used in other place: image ## Test demo after updated Looks like it is no side effect ![2024-04-24 10 17 45](https://github.com/zed-industries/zed/assets/5518/0df4cf06-7448-4014-9df2-f2608a5f5314) --- crates/gpui/src/platform/windows/window.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 301dab271f..f4cf1ded44 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -701,12 +701,13 @@ impl WindowsWindowInner { if let Some(callback) = callbacks.input.as_mut() { let x = lparam.signed_loword() as f32; let y = lparam.signed_hiword() as f32; + let click_count = self.click_state.borrow().current_count; let scale_factor = self.scale_factor.get(); let event = MouseUpEvent { button, position: logical_point(x, y, scale_factor), modifiers: self.current_modifiers(), - click_count: 1, + click_count, }; if callback(PlatformInput::MouseUp(event)).default_prevented { return Some(0); From 544bd490ac1d6123e7b52ebad56b0bf3ce6920ea Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 13:59:14 -0400 Subject: [PATCH 066/101] Extract Elixir extension (#10948) This PR extracts Elixir support into an extension and removes the built-in Elixir support from Zed. As part of this, [Lexical](https://github.com/lexical-lsp/lexical) has been added as an available language server for Elixir. Since the Elixir extension provides three different language servers, you'll need to use the `language_servers` setting to select the one you want to use: #### Elixir LS ```json { "languages": { "Elixir": { "language_servers": [ "elixir-ls", "!next-ls", "!lexical", "..."] } } } ``` #### Next LS ```json { "languages": { "Elixir": { "language_servers": [ "next-ls", "!elixir-ls", "!lexical", "..."] } } } ``` #### Lexical ```json { "languages": { "Elixir": { "language_servers": [ "lexical", "!elixir-ls", "!next-ls", "..."] } } } ``` These can either go in your user settings or your project settings. Release Notes: - Removed built-in support for Elixir, in favor of making it available as an extension. --- Cargo.lock | 14 +- Cargo.toml | 3 +- assets/settings/default.json | 21 - crates/extensions_ui/src/extension_suggest.rs | 1 + crates/languages/Cargo.toml | 5 - crates/languages/src/elixir.rs | 607 ------------------ crates/languages/src/lib.rs | 52 +- extensions/elixir/Cargo.toml | 16 + extensions/elixir/LICENSE-APACHE | 1 + extensions/elixir/extension.toml | 27 + .../elixir/languages}/elixir/brackets.scm | 0 .../elixir/languages}/elixir/config.toml | 0 .../elixir/languages}/elixir/embedding.scm | 0 .../elixir/languages}/elixir/highlights.scm | 0 .../elixir/languages}/elixir/indents.scm | 0 .../elixir/languages}/elixir/injections.scm | 0 .../elixir/languages}/elixir/outline.scm | 0 .../elixir/languages}/elixir/overrides.scm | 0 extensions/elixir/languages/elixir/tasks.json | 28 + .../elixir/languages}/heex/config.toml | 0 .../elixir/languages}/heex/highlights.scm | 0 .../elixir/languages}/heex/injections.scm | 0 .../elixir/languages}/heex/overrides.scm | 0 extensions/elixir/src/elixir.rs | 107 +++ extensions/elixir/src/language_servers.rs | 7 + .../elixir/src/language_servers/elixir_ls.rs | 165 +++++ .../elixir/src/language_servers/lexical.rs | 130 ++++ .../elixir/src/language_servers/next_ls.rs | 176 +++++ 28 files changed, 671 insertions(+), 689 deletions(-) delete mode 100644 crates/languages/src/elixir.rs create mode 100644 extensions/elixir/Cargo.toml create mode 120000 extensions/elixir/LICENSE-APACHE create mode 100644 extensions/elixir/extension.toml rename {crates/languages/src => extensions/elixir/languages}/elixir/brackets.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/config.toml (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/embedding.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/highlights.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/indents.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/injections.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/outline.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/elixir/overrides.scm (100%) create mode 100644 extensions/elixir/languages/elixir/tasks.json rename {crates/languages/src => extensions/elixir/languages}/heex/config.toml (100%) rename {crates/languages/src => extensions/elixir/languages}/heex/highlights.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/heex/injections.scm (100%) rename {crates/languages/src => extensions/elixir/languages}/heex/overrides.scm (100%) create mode 100644 extensions/elixir/src/elixir.rs create mode 100644 extensions/elixir/src/language_servers.rs create mode 100644 extensions/elixir/src/language_servers/elixir_ls.rs create mode 100644 extensions/elixir/src/language_servers/lexical.rs create mode 100644 extensions/elixir/src/language_servers/next_ls.rs diff --git a/Cargo.lock b/Cargo.lock index 20f15bb965..581375fae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5546,12 +5546,9 @@ dependencies = [ "regex", "rope", "rust-embed", - "schemars", "serde", - "serde_derive", "serde_json", "settings", - "shellexpand", "smol", "task", "text", @@ -5562,12 +5559,10 @@ dependencies = [ "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-css", - "tree-sitter-elixir", "tree-sitter-embedded-template", "tree-sitter-go", "tree-sitter-gomod", "tree-sitter-gowork", - "tree-sitter-heex", "tree-sitter-jsdoc", "tree-sitter-json 0.20.0", "tree-sitter-markdown", @@ -10513,7 +10508,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.100" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=7f21c3b98c0749ac192da67a0d65dfe3eabc4a63#7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=528bcd2274814ca53711a57d71d1e3cf7abd73fe#528bcd2274814ca53711a57d71d1e3cf7abd73fe" dependencies = [ "cc", "regex", @@ -12730,6 +12725,13 @@ dependencies = [ "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "zed_elixir" +version = "0.0.1" +dependencies = [ + "zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "zed_elm" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5e2c1b27c5..0c4200f3e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ members = [ "extensions/csharp", "extensions/dart", "extensions/deno", + "extensions/elixir", "extensions/elm", "extensions/emmet", "extensions/erlang", @@ -406,7 +407,7 @@ features = [ ] [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "528bcd2274814ca53711a57d71d1e3cf7abd73fe" } # Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released. pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 5e90ba524c..8b08aa3cf7 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -555,27 +555,6 @@ // Existing terminals will not pick up this change until they are recreated. // "max_scroll_history_lines": 10000, }, - // Settings specific to our elixir integration - "elixir": { - // Change the LSP zed uses for elixir. - // Note that changing this setting requires a restart of Zed - // to take effect. - // - // May take 3 values: - // 1. Use the standard ElixirLS, this is the default - // "lsp": "elixir_ls" - // 2. Use the experimental NextLs - // "lsp": "next_ls", - // 3. Use a language server installed locally on your machine: - // "lsp": { - // "local": { - // "path": "~/next-ls/bin/start", - // "arguments": ["--stdio"] - // } - // }, - // - "lsp": "elixir_ls" - }, "code_actions_on_format": {}, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index d4699fc798..d4f308e97d 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -21,6 +21,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ ("dart", &["dart"]), ("dockerfile", &["Dockerfile"]), ("elisp", &["el"]), + ("elixir", &["ex", "exs", "heex"]), ("elm", &["elm"]), ("erlang", &["erl", "hrl"]), ("fish", &["fish"]), diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 0b299f171c..788ced8362 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -26,12 +26,9 @@ project.workspace = true regex.workspace = true rope.workspace = true rust-embed = "8.2.0" -schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true -shellexpand.workspace = true smol.workspace = true task.workspace = true toml.workspace = true @@ -39,12 +36,10 @@ tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true -tree-sitter-elixir.workspace = true tree-sitter-embedded-template.workspace = true tree-sitter-go.workspace = true tree-sitter-gomod.workspace = true tree-sitter-gowork.workspace = true -tree-sitter-heex.workspace = true tree-sitter-jsdoc.workspace = true tree-sitter-json.workspace = true tree-sitter-markdown.workspace = true diff --git a/crates/languages/src/elixir.rs b/crates/languages/src/elixir.rs deleted file mode 100644 index 1a215bf34a..0000000000 --- a/crates/languages/src/elixir.rs +++ /dev/null @@ -1,607 +0,0 @@ -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use futures::StreamExt; -use gpui::{AppContext, AsyncAppContext, Task}; -pub use language::*; -use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; -use project::project_settings::ProjectSettings; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use serde_json::Value; -use settings::{Settings, SettingsSources}; -use smol::fs::{self, File}; -use std::{ - any::Any, - env::consts, - ops::Deref, - path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, - Arc, - }, -}; -use task::{TaskTemplate, TaskTemplates, VariableName}; -use util::{ - fs::remove_matching, - github::{latest_github_release, GitHubLspBinaryVersion}, - maybe, ResultExt, -}; - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct ElixirSettings { - pub lsp: ElixirLspSetting, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ElixirLspSetting { - ElixirLs, - NextLs, - Local { - path: String, - arguments: Vec, - }, -} - -#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] -pub struct ElixirSettingsContent { - lsp: Option, -} - -impl Settings for ElixirSettings { - const KEY: Option<&'static str> = Some("elixir"); - - type FileContent = ElixirSettingsContent; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - sources.json_merge() - } -} - -pub struct ElixirLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for ElixirLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("elixir-ls".into()) - } - - fn will_start_server( - &self, - delegate: &Arc, - cx: &mut AsyncAppContext, - ) -> Option>> { - static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); - - const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found."; - - let delegate = delegate.clone(); - Some(cx.spawn(|cx| async move { - let elixir_output = smol::process::Command::new("elixir") - .args(["--version"]) - .output() - .await; - if elixir_output.is_err() { - if DID_SHOW_NOTIFICATION - .compare_exchange(false, true, SeqCst, SeqCst) - .is_ok() - { - cx.update(|cx| { - delegate.show_notification(NOTIFICATION_MESSAGE, cx); - })? - } - return Err(anyhow!("cannot run elixir-ls")); - } - - Ok(()) - })) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let http = delegate.http_client(); - let release = latest_github_release("elixir-lsp/elixir-ls", true, false, http).await?; - - let asset_name = format!("elixir-ls-{}.zip", &release.tag_name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no asset found matching {asset_name:?}"))?; - - let version = GitHubLspBinaryVersion { - name: release.tag_name.clone(), - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); - let folder_path = container_dir.join("elixir-ls"); - let binary_path = folder_path.join("language_server.sh"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .context("error downloading release")?; - let mut file = File::create(&zip_path) - .await - .with_context(|| format!("failed to create file {}", zip_path.display()))?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response.body_mut(), &mut file).await?; - - fs::create_dir_all(&folder_path) - .await - .with_context(|| format!("failed to create directory {}", folder_path.display()))?; - let unzip_status = smol::process::Command::new("unzip") - .arg(&zip_path) - .arg("-d") - .arg(&folder_path) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip elixir-ls archive"))?; - } - - remove_matching(&container_dir, |entry| entry != folder_path).await; - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: vec![], - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary_elixir_ls(container_dir).await - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary_elixir_ls(container_dir).await - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - match completion.kind.zip(completion.detail.as_ref()) { - Some((_, detail)) if detail.starts_with("(function)") => { - let text = detail.strip_prefix("(function) ")?; - let filter_range = 0..text.find('(').unwrap_or(text.len()); - let source = Rope::from(format!("def {text}").as_str()); - let runs = language.highlight_text(&source, 4..4 + text.len()); - return Some(CodeLabel { - text: text.to_string(), - runs, - filter_range, - }); - } - Some((_, detail)) if detail.starts_with("(macro)") => { - let text = detail.strip_prefix("(macro) ")?; - let filter_range = 0..text.find('(').unwrap_or(text.len()); - let source = Rope::from(format!("defmacro {text}").as_str()); - let runs = language.highlight_text(&source, 9..9 + text.len()); - return Some(CodeLabel { - text: text.to_string(), - runs, - filter_range, - }); - } - Some(( - CompletionItemKind::CLASS - | CompletionItemKind::MODULE - | CompletionItemKind::INTERFACE - | CompletionItemKind::STRUCT, - _, - )) => { - let filter_range = 0..completion - .label - .find(" (") - .unwrap_or(completion.label.len()); - let text = &completion.label[filter_range.clone()]; - let source = Rope::from(format!("defmodule {text}").as_str()); - let runs = language.highlight_text(&source, 10..10 + text.len()); - return Some(CodeLabel { - text: completion.label.clone(), - runs, - filter_range, - }); - } - _ => {} - } - - None - } - - async fn label_for_symbol( - &self, - name: &str, - kind: SymbolKind, - language: &Arc, - ) -> Option { - let (text, filter_range, display_range) = match kind { - SymbolKind::METHOD | SymbolKind::FUNCTION => { - let text = format!("def {}", name); - let filter_range = 4..4 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => { - let text = format!("defmodule {}", name); - let filter_range = 10..10 + name.len(); - let display_range = 0..filter_range.end; - (text, filter_range, display_range) - } - _ => return None, - }; - - Some(CodeLabel { - runs: language.highlight_text(&text.as_str().into(), display_range.clone()), - text: text[display_range].to_string(), - filter_range, - }) - } - - async fn workspace_configuration( - self: Arc, - _: &Arc, - cx: &mut AsyncAppContext, - ) -> Result { - let settings = cx.update(|cx| { - ProjectSettings::get_global(cx) - .lsp - .get("elixir-ls") - .and_then(|s| s.settings.clone()) - .unwrap_or_default() - })?; - - Ok(serde_json::json!({ - "elixirLS": settings - })) - } -} - -async fn get_cached_server_binary_elixir_ls( - container_dir: PathBuf, -) -> Option { - let server_path = container_dir.join("elixir-ls/language_server.sh"); - if server_path.exists() { - Some(LanguageServerBinary { - path: server_path, - env: None, - arguments: vec![], - }) - } else { - log::error!("missing executable in directory {:?}", server_path); - None - } -} - -pub struct NextLspAdapter; - -#[async_trait(?Send)] -impl LspAdapter for NextLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("next-ls".into()) - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let platform = match consts::ARCH { - "x86_64" => "darwin_amd64", - "aarch64" => "darwin_arm64", - other => bail!("Running on unsupported platform: {other}"), - }; - let release = - latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client()) - .await?; - let version = release.tag_name; - let asset_name = format!("next_ls_{platform}"); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .with_context(|| format!("no asset found matching {asset_name:?}"))?; - let version = GitHubLspBinaryVersion { - name: version, - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - - let binary_path = container_dir.join("next-ls"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - - let mut file = smol::fs::File::create(&binary_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response.body_mut(), &mut file).await?; - - // todo("windows") - #[cfg(not(windows))] - { - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; - } - } - - Ok(LanguageServerBinary { - path: binary_path, - env: None, - arguments: vec!["--stdio".into()], - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary_next(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--stdio".into()]; - binary - }) - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary_next(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--help".into()]; - binary - }) - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - label_for_completion_elixir(completion, language) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol_kind: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_elixir(name, symbol_kind, language) - } -} - -async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option { - maybe!(async { - let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_file() - && entry - .file_name() - .to_str() - .map_or(false, |name| name == "next-ls") - { - last_binary_path = Some(entry.path()); - } - } - - if let Some(path) = last_binary_path { - Ok(LanguageServerBinary { - path, - env: None, - arguments: Vec::new(), - }) - } else { - Err(anyhow!("no cached binary")) - } - }) - .await - .log_err() -} - -pub struct LocalLspAdapter { - pub path: String, - pub arguments: Vec, -} - -#[async_trait(?Send)] -impl LspAdapter for LocalLspAdapter { - fn name(&self) -> LanguageServerName { - LanguageServerName("local-ls".into()) - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - let path = shellexpand::full(&self.path)?; - Ok(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn installation_test_binary(&self, _: PathBuf) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - env: None, - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn label_for_completion( - &self, - completion: &lsp::CompletionItem, - language: &Arc, - ) -> Option { - label_for_completion_elixir(completion, language) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_elixir(name, symbol, language) - } -} - -fn label_for_completion_elixir( - completion: &lsp::CompletionItem, - language: &Arc, -) -> Option { - return Some(CodeLabel { - runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()), - text: completion.label.clone(), - filter_range: 0..completion.label.len(), - }); -} - -fn label_for_symbol_elixir( - name: &str, - _: SymbolKind, - language: &Arc, -) -> Option { - Some(CodeLabel { - runs: language.highlight_text(&name.into(), 0..name.len()), - text: name.to_string(), - filter_range: 0..name.len(), - }) -} - -pub(super) fn elixir_task_context() -> ContextProviderWithTasks { - // Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881 - ContextProviderWithTasks::new(TaskTemplates(vec![ - TaskTemplate { - label: "mix test".to_owned(), - command: "mix".to_owned(), - args: vec!["test".to_owned()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "mix test --failed".to_owned(), - command: "mix".to_owned(), - args: vec!["test".to_owned(), "--failed".to_owned()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: format!("mix test {}", VariableName::Symbol.template_value()), - command: "mix".to_owned(), - args: vec!["test".to_owned(), VariableName::Symbol.template_value()], - ..TaskTemplate::default() - }, - TaskTemplate { - label: format!( - "mix test {}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - command: "mix".to_owned(), - args: vec![ - "test".to_owned(), - format!( - "{}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - ], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "Elixir: break line".to_owned(), - command: "iex".to_owned(), - args: vec![ - "-S".to_owned(), - "mix".to_owned(), - "test".to_owned(), - "-b".to_owned(), - format!( - "{}:{}", - VariableName::File.template_value(), - VariableName::Row.template_value() - ), - ], - ..TaskTemplate::default() - }, - ])) -} diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index ff315727c4..ed9e8c351f 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -3,22 +3,16 @@ use gpui::{AppContext, BorrowAppContext}; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; -use settings::{Settings, SettingsStore}; +use settings::SettingsStore; use smol::stream::StreamExt; use std::{str, sync::Arc}; use util::{asset_str, ResultExt}; -use crate::{ - bash::bash_task_context, elixir::elixir_task_context, python::python_task_context, - rust::RustContextProvider, -}; - -use self::elixir::ElixirSettings; +use crate::{bash::bash_task_context, python::python_task_context, rust::RustContextProvider}; mod bash; mod c; mod css; -mod elixir; mod go; mod json; mod python; @@ -47,14 +41,11 @@ pub fn init( node_runtime: Arc, cx: &mut AppContext, ) { - ElixirSettings::register(cx); - languages.register_native_grammars([ ("bash", tree_sitter_bash::language()), ("c", tree_sitter_c::language()), ("cpp", tree_sitter_cpp::language()), ("css", tree_sitter_css::language()), - ("elixir", tree_sitter_elixir::language()), ( "embedded_template", tree_sitter_embedded_template::language(), @@ -62,7 +53,6 @@ pub fn init( ("go", tree_sitter_go::language()), ("gomod", tree_sitter_gomod::language()), ("gowork", tree_sitter_gowork::language()), - ("heex", tree_sitter_heex::language()), ("jsdoc", tree_sitter_jsdoc::language()), ("json", tree_sitter_json::language()), ("markdown", tree_sitter_markdown::language()), @@ -131,46 +121,9 @@ pub fn init( Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ] ); - - match &ElixirSettings::get(None, cx).lsp { - elixir::ElixirLspSetting::ElixirLs => { - language!( - "elixir", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ], - elixir_task_context() - ); - } - elixir::ElixirLspSetting::NextLs => { - language!( - "elixir", - vec![Arc::new(elixir::NextLspAdapter)], - elixir_task_context() - ); - } - elixir::ElixirLspSetting::Local { path, arguments } => { - language!( - "elixir", - vec![Arc::new(elixir::LocalLspAdapter { - path: path.clone(), - arguments: arguments.clone(), - })], - elixir_task_context() - ); - } - } language!("go", vec![Arc::new(go::GoLspAdapter)]); language!("gomod"); language!("gowork"); - language!( - "heex", - vec![ - Arc::new(elixir::ElixirLspAdapter), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); language!( "json", vec![Arc::new(json::JsonLspAdapter::new( @@ -232,6 +185,7 @@ pub fn init( let tailwind_languages = [ "Astro", + "HEEX", "HTML", "PHP", "Svelte", diff --git a/extensions/elixir/Cargo.toml b/extensions/elixir/Cargo.toml new file mode 100644 index 0000000000..8e8d6c0b2b --- /dev/null +++ b/extensions/elixir/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_elixir" +version = "0.0.1" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/elixir.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/elixir/LICENSE-APACHE b/extensions/elixir/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/elixir/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/elixir/extension.toml b/extensions/elixir/extension.toml new file mode 100644 index 0000000000..7631b0c535 --- /dev/null +++ b/extensions/elixir/extension.toml @@ -0,0 +1,27 @@ +id = "elixir" +name = "Elixir" +description = "Elixir support." +version = "0.0.1" +schema_version = 1 +authors = ["Marshall Bowers "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.elixir-ls] +name = "ElixirLS" +languages = ["Elixir", "HEEX"] + +[language_servers.next-ls] +name = "Next LS" +languages = ["Elixir", "HEEX"] + +[language_servers.lexical] +name = "Lexical" +languages = ["Elixir", "HEEX"] + +[grammars.elixir] +repository = "https://github.com/elixir-lang/tree-sitter-elixir" +commit = "a2861e88a730287a60c11ea9299c033c7d076e30" + +[grammars.heex] +repository = "https://github.com/phoenixframework/tree-sitter-heex" +commit = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" diff --git a/crates/languages/src/elixir/brackets.scm b/extensions/elixir/languages/elixir/brackets.scm similarity index 100% rename from crates/languages/src/elixir/brackets.scm rename to extensions/elixir/languages/elixir/brackets.scm diff --git a/crates/languages/src/elixir/config.toml b/extensions/elixir/languages/elixir/config.toml similarity index 100% rename from crates/languages/src/elixir/config.toml rename to extensions/elixir/languages/elixir/config.toml diff --git a/crates/languages/src/elixir/embedding.scm b/extensions/elixir/languages/elixir/embedding.scm similarity index 100% rename from crates/languages/src/elixir/embedding.scm rename to extensions/elixir/languages/elixir/embedding.scm diff --git a/crates/languages/src/elixir/highlights.scm b/extensions/elixir/languages/elixir/highlights.scm similarity index 100% rename from crates/languages/src/elixir/highlights.scm rename to extensions/elixir/languages/elixir/highlights.scm diff --git a/crates/languages/src/elixir/indents.scm b/extensions/elixir/languages/elixir/indents.scm similarity index 100% rename from crates/languages/src/elixir/indents.scm rename to extensions/elixir/languages/elixir/indents.scm diff --git a/crates/languages/src/elixir/injections.scm b/extensions/elixir/languages/elixir/injections.scm similarity index 100% rename from crates/languages/src/elixir/injections.scm rename to extensions/elixir/languages/elixir/injections.scm diff --git a/crates/languages/src/elixir/outline.scm b/extensions/elixir/languages/elixir/outline.scm similarity index 100% rename from crates/languages/src/elixir/outline.scm rename to extensions/elixir/languages/elixir/outline.scm diff --git a/crates/languages/src/elixir/overrides.scm b/extensions/elixir/languages/elixir/overrides.scm similarity index 100% rename from crates/languages/src/elixir/overrides.scm rename to extensions/elixir/languages/elixir/overrides.scm diff --git a/extensions/elixir/languages/elixir/tasks.json b/extensions/elixir/languages/elixir/tasks.json new file mode 100644 index 0000000000..0e48fcfdc3 --- /dev/null +++ b/extensions/elixir/languages/elixir/tasks.json @@ -0,0 +1,28 @@ +// Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881 +[ + { + "label": "mix test", + "command": "mix", + "args": ["test"] + }, + { + "label": "mix test --failed", + "command": "mix", + "args": ["test", "--failed"] + }, + { + "label": "mix test $ZED_SYMBOL", + "command": "mix", + "args": ["test", "$ZED_SYMBOL"] + }, + { + "label": "mix test $ZED_FILE:$ZED_ROW", + "command": "mix", + "args": ["test", "$ZED_FILE:$ZED_ROW"] + }, + { + "label": "Elixir: break line", + "command": "iex", + "args": ["-S", "mix", "test", "-b", "$ZED_FILE:$ZED_ROW"] + } +] diff --git a/crates/languages/src/heex/config.toml b/extensions/elixir/languages/heex/config.toml similarity index 100% rename from crates/languages/src/heex/config.toml rename to extensions/elixir/languages/heex/config.toml diff --git a/crates/languages/src/heex/highlights.scm b/extensions/elixir/languages/heex/highlights.scm similarity index 100% rename from crates/languages/src/heex/highlights.scm rename to extensions/elixir/languages/heex/highlights.scm diff --git a/crates/languages/src/heex/injections.scm b/extensions/elixir/languages/heex/injections.scm similarity index 100% rename from crates/languages/src/heex/injections.scm rename to extensions/elixir/languages/heex/injections.scm diff --git a/crates/languages/src/heex/overrides.scm b/extensions/elixir/languages/heex/overrides.scm similarity index 100% rename from crates/languages/src/heex/overrides.scm rename to extensions/elixir/languages/heex/overrides.scm diff --git a/extensions/elixir/src/elixir.rs b/extensions/elixir/src/elixir.rs new file mode 100644 index 0000000000..b708cc8470 --- /dev/null +++ b/extensions/elixir/src/elixir.rs @@ -0,0 +1,107 @@ +mod language_servers; + +use zed::lsp::{Completion, Symbol}; +use zed::{serde_json, CodeLabel, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +use crate::language_servers::{ElixirLs, Lexical, NextLs}; + +struct ElixirExtension { + elixir_ls: Option, + next_ls: Option, + lexical: Option, +} + +impl zed::Extension for ElixirExtension { + fn new() -> Self { + Self { + elixir_ls: None, + next_ls: None, + lexical: None, + } + } + + fn language_server_command( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => { + let elixir_ls = self.elixir_ls.get_or_insert_with(|| ElixirLs::new()); + + Ok(zed::Command { + command: elixir_ls.language_server_binary_path(language_server_id, worktree)?, + args: vec![], + env: Default::default(), + }) + } + NextLs::LANGUAGE_SERVER_ID => { + let next_ls = self.next_ls.get_or_insert_with(|| NextLs::new()); + + Ok(zed::Command { + command: next_ls.language_server_binary_path(language_server_id, worktree)?, + args: vec!["--stdio".to_string()], + env: Default::default(), + }) + } + Lexical::LANGUAGE_SERVER_ID => { + let lexical = self.lexical.get_or_insert_with(|| Lexical::new()); + + Ok(zed::Command { + command: lexical.language_server_binary_path(language_server_id, worktree)?, + args: vec![], + env: Default::default(), + }) + } + language_server_id => Err(format!("unknown language server: {language_server_id}")), + } + } + + fn label_for_completion( + &self, + language_server_id: &LanguageServerId, + completion: Completion, + ) -> Option { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => { + self.elixir_ls.as_ref()?.label_for_completion(completion) + } + NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_completion(completion), + Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_completion(completion), + _ => None, + } + } + + fn label_for_symbol( + &self, + language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + match language_server_id.as_ref() { + ElixirLs::LANGUAGE_SERVER_ID => self.elixir_ls.as_ref()?.label_for_symbol(symbol), + NextLs::LANGUAGE_SERVER_ID => self.next_ls.as_ref()?.label_for_symbol(symbol), + Lexical::LANGUAGE_SERVER_ID => self.lexical.as_ref()?.label_for_symbol(symbol), + _ => None, + } + } + + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result> { + match language_server_id.as_ref() { + NextLs::LANGUAGE_SERVER_ID => Ok(Some(serde_json::json!({ + "experimental": { + "completions": { + "enable": true + } + } + }))), + _ => Ok(None), + } + } +} + +zed::register_extension!(ElixirExtension); diff --git a/extensions/elixir/src/language_servers.rs b/extensions/elixir/src/language_servers.rs new file mode 100644 index 0000000000..c2ce97e677 --- /dev/null +++ b/extensions/elixir/src/language_servers.rs @@ -0,0 +1,7 @@ +mod elixir_ls; +mod lexical; +mod next_ls; + +pub use elixir_ls::*; +pub use lexical::*; +pub use next_ls::*; diff --git a/extensions/elixir/src/language_servers/elixir_ls.rs b/extensions/elixir/src/language_servers/elixir_ls.rs new file mode 100644 index 0000000000..1bd179930b --- /dev/null +++ b/extensions/elixir/src/language_servers/elixir_ls.rs @@ -0,0 +1,165 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct ElixirLs { + cached_binary_path: Option, +} + +impl ElixirLs { + pub const LANGUAGE_SERVER_ID: &'static str = "elixir-ls"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = worktree.which("elixir-ls") { + return Ok(path); + } + + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "elixir-lsp/elixir-ls", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let asset_name = format!("elixir-ls-{version}.zip", version = release.version,); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let (platform, _arch) = zed::current_platform(); + let version_dir = format!("elixir-ls-{}", release.version); + let binary_path = format!( + "{version_dir}/language_server.{extension}", + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "sh", + zed::Os::Windows => "bat", + } + ); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::Zip, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + let (code, filter_range, display_range) = match symbol.kind { + SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => { + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + let filter_range = 0..name.len(); + let display_range = defmodule.len()..defmodule.len() + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Function | SymbolKind::Constant => { + let def = "def "; + let code = format!("{def}{name}"); + let filter_range = 0..name.len(); + let display_range = def.len()..def.len() + name.len(); + (code, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + code, + }) + } +} diff --git a/extensions/elixir/src/language_servers/lexical.rs b/extensions/elixir/src/language_servers/lexical.rs new file mode 100644 index 0000000000..b15984498f --- /dev/null +++ b/extensions/elixir/src/language_servers/lexical.rs @@ -0,0 +1,130 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct Lexical { + cached_binary_path: Option, +} + +impl Lexical { + pub const LANGUAGE_SERVER_ID: &'static str = "lexical"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "lexical-lsp/lexical", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let asset_name = format!("lexical-{version}.zip", version = release.version); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("lexical-{}", release.version); + let binary_path = format!("{version_dir}/lexical/bin/start_lexical.sh"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &version_dir, + zed::DownloadedFileType::Zip, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, _symbol: Symbol) -> Option { + None + } +} diff --git a/extensions/elixir/src/language_servers/next_ls.rs b/extensions/elixir/src/language_servers/next_ls.rs new file mode 100644 index 0000000000..14c216f312 --- /dev/null +++ b/extensions/elixir/src/language_servers/next_ls.rs @@ -0,0 +1,176 @@ +use std::fs; + +use zed::lsp::{Completion, CompletionKind, Symbol, SymbolKind}; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +pub struct NextLs { + cached_binary_path: Option, +} + +impl NextLs { + pub const LANGUAGE_SERVER_ID: &'static str = "next-ls"; + + pub fn new() -> Self { + Self { + cached_binary_path: None, + } + } + + pub fn language_server_binary_path( + &mut self, + language_server_id: &LanguageServerId, + _worktree: &zed::Worktree, + ) -> Result { + if let Some(path) = &self.cached_binary_path { + if fs::metadata(path).map_or(false, |stat| stat.is_file()) { + return Ok(path.clone()); + } + } + + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::CheckingForUpdate, + ); + let release = zed::latest_github_release( + "elixir-tools/next-ls", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "next_ls_{os}_{arch}{extension}", + os = match platform { + zed::Os::Mac => "darwin", + zed::Os::Linux => "linux", + zed::Os::Windows => "windows", + }, + arch = match arch { + zed::Architecture::Aarch64 => "arm64", + zed::Architecture::X8664 => "amd64", + zed::Architecture::X86 => + return Err(format!("unsupported architecture: {arch:?}")), + }, + extension = match platform { + zed::Os::Mac | zed::Os::Linux => "", + zed::Os::Windows => ".exe", + } + ); + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; + + let version_dir = format!("next-ls-{}", release.version); + fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?; + + let binary_path = format!("{version_dir}/next-ls"); + + if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { + zed::set_language_server_installation_status( + &language_server_id, + &zed::LanguageServerInstallationStatus::Downloading, + ); + + zed::download_file( + &asset.download_url, + &binary_path, + zed::DownloadedFileType::Uncompressed, + ) + .map_err(|e| format!("failed to download file: {e}"))?; + + zed::make_file_executable(&binary_path)?; + + let entries = + fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; + for entry in entries { + let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; + if entry.file_name().to_str() != Some(&version_dir) { + fs::remove_dir_all(&entry.path()).ok(); + } + } + } + + self.cached_binary_path = Some(binary_path.clone()); + Ok(binary_path) + } + + pub fn label_for_completion(&self, completion: Completion) -> Option { + match completion.kind? { + CompletionKind::Module + | CompletionKind::Class + | CompletionKind::Interface + | CompletionKind::Struct => { + let name = completion.label; + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + defmodule.len()..defmodule.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Function | CompletionKind::Constant => { + let name = completion.label; + let def = "def "; + let code = format!("{def}{name}"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())], + filter_range: (0..name.len()).into(), + }) + } + CompletionKind::Operator => { + let name = completion.label; + let def_a = "def a "; + let code = format!("{def_a}{name} b"); + + Some(CodeLabel { + code, + spans: vec![CodeLabelSpan::code_range( + def_a.len()..def_a.len() + name.len(), + )], + filter_range: (0..name.len()).into(), + }) + } + _ => None, + } + } + + pub fn label_for_symbol(&self, symbol: Symbol) -> Option { + let name = &symbol.name; + + let (code, filter_range, display_range) = match symbol.kind { + SymbolKind::Module | SymbolKind::Class | SymbolKind::Interface | SymbolKind::Struct => { + let defmodule = "defmodule "; + let code = format!("{defmodule}{name}"); + let filter_range = 0..name.len(); + let display_range = defmodule.len()..defmodule.len() + name.len(); + (code, filter_range, display_range) + } + SymbolKind::Function | SymbolKind::Constant => { + let def = "def "; + let code = format!("{def}{name}"); + let filter_range = 0..name.len(); + let display_range = def.len()..def.len() + name.len(); + (code, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(display_range)], + filter_range: filter_range.into(), + code, + }) + } +} From d3f6ca7a1ed257f9957e5ba64c8117acd6a7d4e9 Mon Sep 17 00:00:00 2001 From: James Thurley Date: Thu, 25 Apr 2024 19:20:20 +0100 Subject: [PATCH 067/101] Add @operator, @lifetime and @punctuation.delimiters captures for Rust (#10885) Adds additional captures for theming rust code. I'm uncertain about whether `#` belongs in the `@operator` capture, but I didn't see a more appropriate capture name in my brief hunt in other files. It is the prefix of an `attribute_item`.. suggestions welcome. Release Notes: - Added `@operator`, `@lifetime` and `@punctuation.delimiter` captures to Rust highlights file. --- crates/languages/src/rust/highlights.scm | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index 79a50185aa..b98812fe39 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -64,6 +64,16 @@ "<" @punctuation.bracket ">" @punctuation.bracket) +[ + ";" + "," + "::" +] @punctuation.delimiter + +[ + "#" +] @punctuation.special + [ "as" "async" @@ -122,3 +132,50 @@ (line_comment) (block_comment) ] @comment + +[ + "!" + "!=" + "%" + "%=" + "&" + "&=" + "&&" + "*" + "*=" + "*" + "+" + "+=" + "," + "-" + "-=" + "->" + "." + ".." + "..=" + "..." + "/" + "/=" + ":" + ";" + "<<" + "<<=" + "<" + "<=" + "=" + "==" + "=>" + ">" + ">=" + ">>" + ">>=" + "@" + "^" + "^=" + "|" + "|=" + "||" + "?" +] @operator + +(lifetime) @lifetime From 7005f0b4248af9bc22cb7336b63219656a0bb2b3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 14:30:31 -0400 Subject: [PATCH 068/101] Remove outdated instructions for adding languages (#11005) This PR removes the outdated comment regarding adding languages to Zed. New languages should be added as extensions. Release Notes: - N/A --- crates/languages/src/lib.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index ed9e8c351f..3223aa8749 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -22,15 +22,6 @@ mod tailwind; mod typescript; mod yaml; -// 1. Add tree-sitter-{language} parser to zed crate -// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below -// 3. Add config.toml to the newly created language directory using existing languages as a template -// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file. -// Note: github highlights take the last match while zed takes the first -// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs, -// and autoclosing brackets respectively -// 6. If the language has injections add an injections.scm query file - #[derive(RustEmbed)] #[folder = "src/"] #[exclude = "*.rs"] From f176e8f0e45c37de19d30f9508db81bab6e663df Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 25 Apr 2024 13:03:43 -0700 Subject: [PATCH 069/101] Accept `View`s on `LanguageModelTool`s (#10956) Creates a `ToolView` trait to allow interactivity. This brings expanding and collapsing to the excerpts from project index searches. Release Notes: - N/A --------- Co-authored-by: Nathan Co-authored-by: Max Brunsfeld --- .../examples/chat-with-functions.rs | 54 +++-- crates/assistant2/src/assistant2.rs | 17 +- crates/assistant2/src/tools.rs | 172 ++++++++++------ crates/assistant_tooling/src/registry.rs | 192 ++++++++---------- crates/assistant_tooling/src/tool.rs | 102 +++------- 5 files changed, 268 insertions(+), 269 deletions(-) diff --git a/crates/assistant2/examples/chat-with-functions.rs b/crates/assistant2/examples/chat-with-functions.rs index 15d3c968a4..0a6ecbb02b 100644 --- a/crates/assistant2/examples/chat-with-functions.rs +++ b/crates/assistant2/examples/chat-with-functions.rs @@ -1,4 +1,5 @@ -use anyhow::Context as _; +/// This example creates a basic Chat UI with a function for rolling a die. +use anyhow::{Context as _, Result}; use assets::Assets; use assistant2::AssistantPanel; use assistant_tooling::{LanguageModelTool, ToolRegistry}; @@ -83,9 +84,32 @@ struct DiceRoll { rolls: Vec, } +pub struct DiceView { + result: Result, +} + +impl Render for DiceView { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + let output = match &self.result { + Ok(output) => output, + Err(_) => return "Somehow dice failed 🎲".into_any_element(), + }; + + h_flex() + .children( + output + .rolls + .iter() + .map(|roll| div().p_2().child(roll.render())), + ) + .into_any_element() + } +} + impl LanguageModelTool for RollDiceTool { type Input = DiceParams; type Output = DiceRoll; + type View = DiceView; fn name(&self) -> String { "roll_dice".to_string() @@ -110,23 +134,21 @@ impl LanguageModelTool for RollDiceTool { return Task::ready(Ok(DiceRoll { rolls })); } - fn render( - _tool_call_id: &str, - _input: &Self::Input, - output: &Self::Output, - _cx: &mut WindowContext, - ) -> gpui::AnyElement { - h_flex() - .children( - output - .rolls - .iter() - .map(|roll| div().p_2().child(roll.render())), - ) - .into_any_element() + fn new_view( + _tool_call_id: String, + _input: Self::Input, + result: Result, + cx: &mut WindowContext, + ) -> gpui::View { + cx.new_view(|_cx| DiceView { result }) } - fn format(_input: &Self::Input, output: &Self::Output) -> String { + fn format(_: &Self::Input, output: &Result) -> String { + let output = match output { + Ok(output) => output, + Err(_) => return "Somehow dice failed 🎲".to_string(), + }; + let mut result = String::new(); for roll in &output.rolls { let die = &roll.die; diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index b89291bd13..22d05a3fc5 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -322,9 +322,11 @@ impl AssistantChat { }; call_count += 1; + let messages = this.completion_messages(cx); + CompletionProvider::get(cx).complete( this.model.clone(), - this.completion_messages(cx), + messages, Vec::new(), 1.0, definitions, @@ -407,6 +409,10 @@ impl AssistantChat { } let tools = join_all(tool_tasks.into_iter()).await; + // If the WindowContext went away for any tool's view we don't include it + // especially since the below call would fail for the same reason. + let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect(); + this.update(cx, |this, cx| { if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) = this.messages.last_mut() @@ -561,10 +567,9 @@ impl AssistantChat { let result = &tool_call.result; let name = tool_call.name.clone(); match result { - Some(result) => div() - .p_2() - .child(result.render(&name, &tool_call.id, cx)) - .into_any(), + Some(result) => { + div().p_2().child(result.into_any_element(&name)).into_any() + } None => div() .p_2() .child(Label::new(name).color(Color::Modified)) @@ -577,7 +582,7 @@ impl AssistantChat { } } - fn completion_messages(&self, cx: &WindowContext) -> Vec { + fn completion_messages(&self, cx: &mut WindowContext) -> Vec { let mut completion_messages = Vec::new(); for message in &self.messages { diff --git a/crates/assistant2/src/tools.rs b/crates/assistant2/src/tools.rs index ffd5e42bfa..3e86e72168 100644 --- a/crates/assistant2/src/tools.rs +++ b/crates/assistant2/src/tools.rs @@ -1,10 +1,10 @@ use anyhow::Result; use assistant_tooling::LanguageModelTool; -use gpui::{prelude::*, AnyElement, AppContext, Model, Task}; +use gpui::{prelude::*, AppContext, Model, Task}; use project::Fs; use schemars::JsonSchema; use semantic_index::ProjectIndex; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::sync::Arc; use ui::{ div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString, @@ -14,11 +14,13 @@ use util::ResultExt as _; const DEFAULT_SEARCH_LIMIT: usize = 20; -#[derive(Serialize, Clone)] +#[derive(Clone)] pub struct CodebaseExcerpt { path: SharedString, text: SharedString, score: f32, + element_id: ElementId, + expanded: bool, } // Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model. @@ -32,6 +34,79 @@ pub struct CodebaseQuery { limit: Option, } +pub struct ProjectIndexView { + input: CodebaseQuery, + output: Result>, +} + +impl ProjectIndexView { + fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext) { + if let Ok(excerpts) = &mut self.output { + if let Some(excerpt) = excerpts + .iter_mut() + .find(|excerpt| excerpt.element_id == element_id) + { + excerpt.expanded = !excerpt.expanded; + cx.notify(); + } + } + } +} + +impl Render for ProjectIndexView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let query = self.input.query.clone(); + + let result = &self.output; + + let excerpts = match result { + Err(err) => { + return div().child(Label::new(format!("Error: {}", err)).color(Color::Error)); + } + Ok(excerpts) => excerpts, + }; + + div() + .v_flex() + .gap_2() + .child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .child( + h_flex() + .child(Label::new("Query: ").color(Color::Modified)) + .child(Label::new(query).color(Color::Muted)), + ), + ) + .children(excerpts.iter().map(|excerpt| { + let element_id = excerpt.element_id.clone(); + let expanded = excerpt.expanded; + + CollapsibleContainer::new(element_id.clone(), expanded) + .start_slot( + h_flex() + .gap_1() + .child(Icon::new(IconName::File).color(Color::Muted)) + .child(Label::new(excerpt.path.clone()).color(Color::Muted)), + ) + .on_click(cx.listener(move |this, _, cx| { + this.toggle_expanded(element_id.clone(), cx); + })) + .child( + div() + .p_2() + .rounded_md() + .bg(cx.theme().colors().editor_background) + .child( + excerpt.text.clone(), // todo!(): Show as an editor block + ), + ) + })) + } +} + pub struct ProjectIndexTool { project_index: Model, fs: Arc, @@ -47,6 +122,7 @@ impl ProjectIndexTool { impl LanguageModelTool for ProjectIndexTool { type Input = CodebaseQuery; type Output = Vec; + type View = ProjectIndexView; fn name(&self) -> String { "query_codebase".to_string() @@ -90,6 +166,8 @@ impl LanguageModelTool for ProjectIndexTool { } anyhow::Ok(CodebaseExcerpt { + element_id: ElementId::Name(nanoid::nanoid!().into()), + expanded: false, path: path.to_string_lossy().to_string().into(), text: SharedString::from(text[start..end].to_string()), score: result.score, @@ -106,71 +184,37 @@ impl LanguageModelTool for ProjectIndexTool { }) } - fn render( - _tool_call_id: &str, - input: &Self::Input, - excerpts: &Self::Output, + fn new_view( + _tool_call_id: String, + input: Self::Input, + output: Result, cx: &mut WindowContext, - ) -> AnyElement { - let query = input.query.clone(); - - div() - .v_flex() - .gap_2() - .child( - div() - .p_2() - .rounded_md() - .bg(cx.theme().colors().editor_background) - .child( - h_flex() - .child(Label::new("Query: ").color(Color::Modified)) - .child(Label::new(query).color(Color::Muted)), - ), - ) - .children(excerpts.iter().map(|excerpt| { - // This render doesn't have state/model, so we can't use the listener - // let expanded = excerpt.expanded; - // let element_id = excerpt.element_id.clone(); - let element_id = ElementId::Name(nanoid::nanoid!().into()); - let expanded = false; - - CollapsibleContainer::new(element_id.clone(), expanded) - .start_slot( - h_flex() - .gap_1() - .child(Icon::new(IconName::File).color(Color::Muted)) - .child(Label::new(excerpt.path.clone()).color(Color::Muted)), - ) - // .on_click(cx.listener(move |this, _, cx| { - // this.toggle_expanded(element_id.clone(), cx); - // })) - .child( - div() - .p_2() - .rounded_md() - .bg(cx.theme().colors().editor_background) - .child( - excerpt.text.clone(), // todo!(): Show as an editor block - ), - ) - })) - .into_any_element() + ) -> gpui::View { + cx.new_view(|_cx| ProjectIndexView { input, output }) } - fn format(_input: &Self::Input, excerpts: &Self::Output) -> String { - let mut body = "Semantic search results:\n".to_string(); + fn format(_input: &Self::Input, output: &Result) -> String { + match &output { + Ok(excerpts) => { + if excerpts.len() == 0 { + return "No results found".to_string(); + } - for excerpt in excerpts { - body.push_str("Excerpt from "); - body.push_str(excerpt.path.as_ref()); - body.push_str(", score "); - body.push_str(&excerpt.score.to_string()); - body.push_str(":\n"); - body.push_str("~~~\n"); - body.push_str(excerpt.text.as_ref()); - body.push_str("~~~\n"); + let mut body = "Semantic search results:\n".to_string(); + + for excerpt in excerpts { + body.push_str("Excerpt from "); + body.push_str(excerpt.path.as_ref()); + body.push_str(", score "); + body.push_str(&excerpt.score.to_string()); + body.push_str(":\n"); + body.push_str("~~~\n"); + body.push_str(excerpt.text.as_ref()); + body.push_str("~~~\n"); + } + body + } + Err(err) => format!("Error: {}", err), } - body } } diff --git a/crates/assistant_tooling/src/registry.rs b/crates/assistant_tooling/src/registry.rs index ac5930cac4..6a3bc313cd 100644 --- a/crates/assistant_tooling/src/registry.rs +++ b/crates/assistant_tooling/src/registry.rs @@ -1,13 +1,16 @@ use anyhow::{anyhow, Result}; -use gpui::{AnyElement, AppContext, Task, WindowContext}; -use std::{any::Any, collections::HashMap}; +use gpui::{Task, WindowContext}; +use std::collections::HashMap; use crate::tool::{ LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition, }; pub struct ToolRegistry { - tools: HashMap Task>>, + tools: HashMap< + String, + Box Task>>, + >, definitions: Vec, } @@ -24,77 +27,45 @@ impl ToolRegistry { } pub fn register(&mut self, tool: T) -> Result<()> { - fn render( - tool_call_id: &str, - input: &Box, - output: &Box, - cx: &mut WindowContext, - ) -> AnyElement { - T::render( - tool_call_id, - input.as_ref().downcast_ref::().unwrap(), - output.as_ref().downcast_ref::().unwrap(), - cx, - ) - } - - fn format( - input: &Box, - output: &Box, - ) -> String { - T::format( - input.as_ref().downcast_ref::().unwrap(), - output.as_ref().downcast_ref::().unwrap(), - ) - } - self.definitions.push(tool.definition()); let name = tool.name(); let previous = self.tools.insert( name.clone(), - Box::new(move |tool_call: &ToolFunctionCall, cx: &AppContext| { - let name = tool_call.name.clone(); - let arguments = tool_call.arguments.clone(); - let id = tool_call.id.clone(); + // registry.call(tool_call, cx) + Box::new( + move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| { + let name = tool_call.name.clone(); + let arguments = tool_call.arguments.clone(); + let id = tool_call.id.clone(); - let Ok(input) = serde_json::from_str::(arguments.as_str()) else { - return Task::ready(ToolFunctionCall { - id, - name: name.clone(), - arguments, - result: Some(ToolFunctionCallResult::ParsingFailed), - }); - }; - - let result = tool.execute(&input, cx); - - cx.spawn(move |_cx| async move { - match result.await { - Ok(result) => { - let result: T::Output = result; - ToolFunctionCall { - id, - name: name.clone(), - arguments, - result: Some(ToolFunctionCallResult::Finished { - input: Box::new(input), - output: Box::new(result), - render_fn: render::, - format_fn: format::, - }), - } - } - Err(_error) => ToolFunctionCall { + let Ok(input) = serde_json::from_str::(arguments.as_str()) else { + return Task::ready(Ok(ToolFunctionCall { id, name: name.clone(), arguments, - result: Some(ToolFunctionCallResult::ExecutionFailed { - input: Box::new(input), + result: Some(ToolFunctionCallResult::ParsingFailed), + })); + }; + + let result = tool.execute(&input, cx); + + cx.spawn(move |mut cx| async move { + let result: Result = result.await; + let for_model = T::format(&input, &result); + let view = cx.update(|cx| T::new_view(id.clone(), input, result, cx))?; + + Ok(ToolFunctionCall { + id, + name: name.clone(), + arguments, + result: Some(ToolFunctionCallResult::Finished { + view: view.into(), + for_model, }), - }, - } - }) - }), + }) + }) + }, + ), ); if previous.is_some() { @@ -104,7 +75,12 @@ impl ToolRegistry { Ok(()) } - pub fn call(&self, tool_call: &ToolFunctionCall, cx: &AppContext) -> Task { + /// Task yields an error if the window for the given WindowContext is closed before the task completes. + pub fn call( + &self, + tool_call: &ToolFunctionCall, + cx: &mut WindowContext, + ) -> Task> { let name = tool_call.name.clone(); let arguments = tool_call.arguments.clone(); let id = tool_call.id.clone(); @@ -113,12 +89,12 @@ impl ToolRegistry { Some(tool) => tool, None => { let name = name.clone(); - return Task::ready(ToolFunctionCall { + return Task::ready(Ok(ToolFunctionCall { id, name: name.clone(), arguments, result: Some(ToolFunctionCallResult::NoSuchTool), - }); + })); } }; @@ -128,12 +104,10 @@ impl ToolRegistry { #[cfg(test)] mod test { - use super::*; - + use gpui::View; + use gpui::{div, prelude::*, Render, TestAppContext}; use schemars::schema_for; - - use gpui::{div, AnyElement, Element, ParentElement, TestAppContext, WindowContext}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -155,9 +129,20 @@ mod test { unit: String, } + struct WeatherView { + result: WeatherResult, + } + + impl Render for WeatherView { + fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl IntoElement { + div().child(format!("temperature: {}", self.result.temperature)) + } + } + impl LanguageModelTool for WeatherTool { type Input = WeatherQuery; type Output = WeatherResult; + type View = WeatherView; fn name(&self) -> String { "get_current_weather".to_string() @@ -167,7 +152,11 @@ mod test { "Fetches the current weather for a given location.".to_string() } - fn execute(&self, input: &WeatherQuery, _cx: &AppContext) -> Task> { + fn execute( + &self, + input: &Self::Input, + _cx: &gpui::AppContext, + ) -> Task> { let _location = input.location.clone(); let _unit = input.unit.clone(); @@ -176,25 +165,20 @@ mod test { Task::ready(Ok(weather)) } - fn render( - _tool_call_id: &str, - _input: &Self::Input, - output: &Self::Output, - _cx: &mut WindowContext, - ) -> AnyElement { - div() - .child(format!( - "The current temperature in {} is {} {}", - output.location, output.temperature, output.unit - )) - .into_any() + fn new_view( + _tool_call_id: String, + _input: Self::Input, + result: Result, + cx: &mut WindowContext, + ) -> View { + cx.new_view(|_cx| { + let result = result.unwrap(); + WeatherView { result } + }) } - fn format(_input: &Self::Input, output: &Self::Output) -> String { - format!( - "The current temperature in {} is {} {}", - output.location, output.temperature, output.unit - ) + fn format(_: &Self::Input, output: &Result) -> String { + serde_json::to_string(&output.as_ref().unwrap()).unwrap() } } @@ -214,20 +198,20 @@ mod test { registry.register(tool).unwrap(); - let _result = cx - .update(|cx| { - registry.call( - &ToolFunctionCall { - name: "get_current_weather".to_string(), - arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"# - .to_string(), - id: "test-123".to_string(), - result: None, - }, - cx, - ) - }) - .await; + // let _result = cx + // .update(|cx| { + // registry.call( + // &ToolFunctionCall { + // name: "get_current_weather".to_string(), + // arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"# + // .to_string(), + // id: "test-123".to_string(), + // result: None, + // }, + // cx, + // ) + // }) + // .await; // assert!(result.is_ok()); // let result = result.unwrap(); diff --git a/crates/assistant_tooling/src/tool.rs b/crates/assistant_tooling/src/tool.rs index a3b021a04e..8a1ffcf9d4 100644 --- a/crates/assistant_tooling/src/tool.rs +++ b/crates/assistant_tooling/src/tool.rs @@ -1,11 +1,8 @@ use anyhow::Result; -use gpui::{div, AnyElement, AppContext, Element, ParentElement as _, Task, WindowContext}; +use gpui::{AnyElement, AnyView, AppContext, IntoElement as _, Render, Task, View, WindowContext}; use schemars::{schema::RootSchema, schema_for, JsonSchema}; use serde::Deserialize; -use std::{ - any::Any, - fmt::{Debug, Display}, -}; +use std::fmt::Display; #[derive(Default, Deserialize)] pub struct ToolFunctionCall { @@ -19,71 +16,29 @@ pub struct ToolFunctionCall { pub enum ToolFunctionCallResult { NoSuchTool, ParsingFailed, - ExecutionFailed { - input: Box, - }, - Finished { - input: Box, - output: Box, - render_fn: fn( - // tool_call_id - &str, - // LanguageModelTool::Input - &Box, - // LanguageModelTool::Output - &Box, - &mut WindowContext, - ) -> AnyElement, - format_fn: fn( - // LanguageModelTool::Input - &Box, - // LanguageModelTool::Output - &Box, - ) -> String, - }, + Finished { for_model: String, view: AnyView }, } impl ToolFunctionCallResult { - pub fn render( - &self, - tool_name: &str, - tool_call_id: &str, - cx: &mut WindowContext, - ) -> AnyElement { + pub fn format(&self, name: &String) -> String { match self { - ToolFunctionCallResult::NoSuchTool => { - div().child(format!("no such tool {tool_name}")).into_any() + ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"), + ToolFunctionCallResult::ParsingFailed => { + format!("Unable to parse arguments for {name}") } - ToolFunctionCallResult::ParsingFailed => div() - .child(format!("failed to parse input for tool {tool_name}")) - .into_any(), - ToolFunctionCallResult::ExecutionFailed { .. } => div() - .child(format!("failed to execute tool {tool_name}")) - .into_any(), - ToolFunctionCallResult::Finished { - input, - output, - render_fn, - .. - } => render_fn(tool_call_id, input, output, cx), + ToolFunctionCallResult::Finished { for_model, .. } => for_model.clone(), } } - pub fn format(&self, tool: &str) -> String { + pub fn into_any_element(&self, name: &String) -> AnyElement { match self { - ToolFunctionCallResult::NoSuchTool => format!("no such tool {tool}"), + ToolFunctionCallResult::NoSuchTool => { + format!("Language Model attempted to call {name}").into_any_element() + } ToolFunctionCallResult::ParsingFailed => { - format!("failed to parse input for tool {tool}") + format!("Language Model called {name} with bad arguments").into_any_element() } - ToolFunctionCallResult::ExecutionFailed { input: _input } => { - format!("failed to execute tool {tool}") - } - ToolFunctionCallResult::Finished { - input, - output, - format_fn, - .. - } => format_fn(input, output), + ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(), } } } @@ -105,19 +60,6 @@ impl Display for ToolFunctionDefinition { } } -impl Debug for ToolFunctionDefinition { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let schema = serde_json::to_string(&self.parameters).ok(); - let schema = schema.unwrap_or("None".to_string()); - - f.debug_struct("ToolFunctionDefinition") - .field("name", &self.name) - .field("description", &self.description) - .field("parameters", &schema) - .finish() - } -} - pub trait LanguageModelTool { /// The input type that will be passed in to `execute` when the tool is called /// by the language model. @@ -126,6 +68,8 @@ pub trait LanguageModelTool { /// The output returned by executing the tool. type Output: 'static; + type View: Render; + /// The name of the tool is exposed to the language model to allow /// the model to pick which tools to use. As this name is used to /// identify the tool within a tool registry, it should be unique. @@ -149,12 +93,12 @@ pub trait LanguageModelTool { /// Execute the tool fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task>; - fn render( - tool_call_id: &str, - input: &Self::Input, - output: &Self::Output, - cx: &mut WindowContext, - ) -> AnyElement; + fn format(input: &Self::Input, output: &Result) -> String; - fn format(input: &Self::Input, output: &Self::Output) -> String; + fn new_view( + tool_call_id: String, + input: Self::Input, + output: Result, + cx: &mut WindowContext, + ) -> View; } From c833a7e662e9509fcf18a1520259ca5d4de7b1f9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Apr 2024 14:21:01 -0600 Subject: [PATCH 070/101] Don't use fancy cursors for non-vim people (#11010) Release Notes: - N/A --- crates/vim/src/editor_events.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index ee5f4cde09..9ddcea3852 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -42,6 +42,9 @@ fn focused(editor: View, cx: &mut WindowContext) { fn blurred(editor: View, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { + if !vim.enabled { + return; + } if let Some(previous_editor) = vim.active_editor.clone() { vim.stop_recording_immediately(NormalBefore.boxed_clone()); if previous_editor From 3eac581a62f735b3c0a4caebbd0a19215c6125c6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 17:29:47 -0400 Subject: [PATCH 071/101] Allow controlling Tailwind via the `language_servers` setting (#11012) This PR adds the ability for the Tailwind language server (`tailwindcss-language-server`) to be controlled by the `language_servers` setting. Now in your settings you can indicate that the Tailwind language server should be used for a given language, even if that language does not have the Tailwind language server registered for it already: ```json { "languages": { "My Language": { "language_servers": ["tailwindcss-language-server", "..."] } } } ``` Release Notes: - N/A --- crates/language/src/language_registry.rs | 35 +++++++++++++++++++ crates/language/src/language_settings.rs | 19 +++++++++++ crates/languages/src/lib.rs | 43 +++++++++++++++++------- crates/project/src/project.rs | 34 +++++++++++++++---- 4 files changed, 113 insertions(+), 18 deletions(-) diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 6d30a80bac..74fbc588de 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -46,6 +46,8 @@ struct LanguageRegistryState { available_languages: Vec, grammars: HashMap, AvailableGrammar>, lsp_adapters: HashMap, Vec>>, + available_lsp_adapters: + HashMap Arc + 'static + Send + Sync>>, loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, @@ -153,6 +155,7 @@ impl LanguageRegistry { language_settings: Default::default(), loading_languages: Default::default(), lsp_adapters: Default::default(), + available_lsp_adapters: HashMap::default(), subscription: watch::channel(), theme: Default::default(), version: 0, @@ -213,6 +216,38 @@ impl LanguageRegistry { ) } + /// Registers an available language server adapter. + /// + /// The language server is registered under the language server name, but + /// not bound to a particular language. + /// + /// When a language wants to load this particular language server, it will + /// invoke the `load` function. + pub fn register_available_lsp_adapter( + &self, + name: LanguageServerName, + load: impl Fn() -> Arc + 'static + Send + Sync, + ) { + self.state.write().available_lsp_adapters.insert( + name, + Arc::new(move || { + let lsp_adapter = load(); + CachedLspAdapter::new(lsp_adapter, true) + }), + ); + } + + /// Loads the language server adapter for the language server with the given name. + pub fn load_available_lsp_adapter( + &self, + name: &LanguageServerName, + ) -> Option> { + let state = self.state.read(); + let load_lsp_adapter = state.available_lsp_adapters.get(name)?; + + Some(load_lsp_adapter()) + } + pub fn register_lsp_adapter(&self, language_name: Arc, adapter: Arc) { self.state .write() diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index e65b823bc1..16b8ab0eb2 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -789,5 +789,24 @@ mod tests { ), language_server_names(&["deno", "eslint", "tailwind"]) ); + + // Adding a language server not in the list of available languages servers adds it to the list. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[ + "my-cool-language-server".into(), + LanguageSettings::REST_OF_LANGUAGE_SERVERS.into() + ], + &available_language_servers + ), + language_server_names(&[ + "my-cool-language-server", + "typescript-language-server", + "biome", + "deno", + "eslint", + "tailwind", + ]) + ); } } diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 3223aa8749..3810ba16eb 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -107,10 +107,7 @@ pub fn init( language!("cpp", vec![Arc::new(c::CLspAdapter)]); language!( "css", - vec![ - Arc::new(css::CssLspAdapter::new(node_runtime.clone())), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] + vec![Arc::new(css::CssLspAdapter::new(node_runtime.clone())),] ); language!("go", vec![Arc::new(go::GoLspAdapter)]); language!("gomod"); @@ -160,13 +157,7 @@ pub fn init( ))] ); language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]); - language!( - "erb", - vec![ - Arc::new(ruby::RubyLanguageServer), - Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), - ] - ); + language!("erb", vec![Arc::new(ruby::RubyLanguageServer),]); language!("regex"); language!( "yaml", @@ -174,14 +165,42 @@ pub fn init( ); language!("proto"); + // Register Tailwind globally as an available language server. + // + // This will allow users to add Tailwind support for a given language via + // the `language_servers` setting: + // + // ```json + // { + // "languages": { + // "My Language": { + // "language_servers": ["tailwindcss-language-server", "..."] + // } + // } + // } + // ``` + languages.register_available_lsp_adapter( + LanguageServerName("tailwindcss-language-server".into()), + { + let node_runtime = node_runtime.clone(); + move || Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())) + }, + ); + + // Register Tailwind for the existing languages that should have it by default. + // + // This can be driven by the `language_servers` setting once we have a way for + // extensions to provide their own default value for that setting. let tailwind_languages = [ "Astro", + "CSS", + "ERB", "HEEX", "HTML", + "JavaScript", "PHP", "Svelte", "TSX", - "JavaScript", "Vue.js", ]; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 444393f17c..34a6359db4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3066,12 +3066,34 @@ impl Project { .map(|lsp_adapter| lsp_adapter.name.clone()) .collect::>(); - let enabled_language_servers = + let desired_language_servers = settings.customized_language_servers(&available_language_servers); - let enabled_lsp_adapters = available_lsp_adapters - .into_iter() - .filter(|adapter| enabled_language_servers.contains(&adapter.name)) - .collect::>(); + + let mut enabled_lsp_adapters: Vec> = Vec::new(); + for desired_language_server in desired_language_servers { + if let Some(adapter) = available_lsp_adapters + .iter() + .find(|adapter| adapter.name == desired_language_server) + { + enabled_lsp_adapters.push(adapter.clone()); + continue; + } + + if let Some(adapter) = self + .languages + .load_available_lsp_adapter(&desired_language_server) + { + self.languages() + .register_lsp_adapter(language.name(), adapter.adapter.clone()); + enabled_lsp_adapters.push(adapter); + continue; + } + + log::warn!( + "no language server found matching '{}'", + desired_language_server.0 + ); + } log::info!( "starting language servers for {language}: {adapters}", @@ -3083,7 +3105,7 @@ impl Project { ); for adapter in enabled_lsp_adapters { - self.start_language_server(worktree, adapter.clone(), language.clone(), cx); + self.start_language_server(worktree, adapter, language.clone(), cx); } } From 7af96a15fe67964e5d1a595dee31ef1e25554047 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 17:30:47 -0400 Subject: [PATCH 072/101] Fix typo in comment --- crates/language/src/language_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 16b8ab0eb2..e628b3d1c0 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -790,7 +790,7 @@ mod tests { language_server_names(&["deno", "eslint", "tailwind"]) ); - // Adding a language server not in the list of available languages servers adds it to the list. + // Adding a language server not in the list of available language servers adds it to the list. assert_eq!( LanguageSettings::resolve_language_servers( &[ From 4c780568bc57e13c1561efdcf64171e5b5af20b8 Mon Sep 17 00:00:00 2001 From: Michael Angerman <1809991+stormasm@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:37:47 -0700 Subject: [PATCH 073/101] storybook: Fix Backspace in Auto Height Editor and Picker stories (#11011) Currently in the *Auto Height Editor* story the backspace key is not working when you type into the Editor. The same thing is true for the *Picker* story as one is not able to backspace... By adding an entry in the keymap file ```rust assets/keymaps/storybook.json ``` both of these issues are solved... Release Notes: - N/A --- assets/keymaps/storybook.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/storybook.json b/assets/keymaps/storybook.json index 117bbde09b..5e375821e0 100644 --- a/assets/keymaps/storybook.json +++ b/assets/keymaps/storybook.json @@ -17,7 +17,11 @@ "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "cmd-q": "storybook::Quit" + "cmd-q": "storybook::Quit", + "backspace": "editor::Backspace", + "delete": "editor::Delete", + "left": "editor::MoveLeft", + "right": "editor::MoveRight" } } ] From 366d7e77281dde3e029fcb01f7fc9f3ec79543ee Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 25 Apr 2024 17:42:53 -0400 Subject: [PATCH 074/101] Break typography styles out of `StyledExt` (#11013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Centralizes typography-related UI styles and methods in `styles/typography.rs` - Breaks the typography-related styles out of `StyledExt`. This means we add a `StyledTypography` trait – this should more or less be an invisible change as we publish it in the prelude. - adds the ability to easily grab the UI or Buffer font sizes (`ui_font_size`, `buffer_font_size`) with `TextSize::UI`, `TextSize::Editor` Release Notes: - N/A --- crates/breadcrumbs/src/breadcrumbs.rs | 2 +- crates/collab_ui/src/chat_panel.rs | 12 +-- .../src/chat_panel/message_editor.rs | 4 +- .../src/notifications/collab_notification.rs | 2 +- crates/outline/src/outline.rs | 2 +- crates/ui/src/components/keybinding.rs | 2 +- crates/ui/src/components/label/label_like.rs | 8 +- crates/ui/src/components/tooltip.rs | 2 +- crates/ui/src/prelude.rs | 2 +- crates/ui/src/styled_ext.rs | 64 +------------- crates/ui/src/styles/typography.rs | 84 ++++++++++++++++++- 11 files changed, 100 insertions(+), 84 deletions(-) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 89edd3606b..d70b1cb227 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -33,7 +33,7 @@ impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { const MAX_SEGMENTS: usize = 12; - let element = h_flex().text_ui(); + let element = h_flex().text_ui(cx); let Some(active_item) = self.active_item.as_ref() else { return element; }; diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index ef37ce653b..d5a1374777 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -315,7 +315,7 @@ impl ChatPanel { None => { return div().child( h_flex() - .text_ui_xs() + .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() @@ -350,7 +350,7 @@ impl ChatPanel { div().child( h_flex() .id(message_element_id) - .text_ui_xs() + .text_ui_xs(cx) .my_0p5() .px_0p5() .gap_x_1() @@ -495,7 +495,7 @@ impl ChatPanel { |this| { this.child( h_flex() - .text_ui_sm() + .text_ui_sm(cx) .child( div().absolute().child( Avatar::new(message.sender.avatar_uri.clone()) @@ -539,7 +539,7 @@ impl ChatPanel { el.child( v_flex() .w_full() - .text_ui_sm() + .text_ui_sm(cx) .id(element_id) .child(text.element("body".into(), cx)), ) @@ -562,7 +562,7 @@ impl ChatPanel { div() .px_1() .rounded_md() - .text_ui_xs() + .text_ui_xs(cx) .bg(cx.theme().colors().background) .child("New messages"), ) @@ -1003,7 +1003,7 @@ impl Render for ChatPanel { el.child( h_flex() .px_2() - .text_ui_xs() + .text_ui_xs(cx) .justify_between() .border_t_1() .border_color(cx.theme().colors().border) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index c1c21d6ab7..f00575c8fd 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -18,7 +18,7 @@ use project::{search::SearchQuery, Completion}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; use theme::ThemeSettings; -use ui::{prelude::*, UiTextSize}; +use ui::{prelude::*, TextSize}; use crate::panel_settings::MessageEditorSettings; @@ -523,7 +523,7 @@ impl Render for MessageEditor { }, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features, - font_size: UiTextSize::Small.rems().into(), + font_size: TextSize::Small.rems(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.3), diff --git a/crates/collab_ui/src/notifications/collab_notification.rs b/crates/collab_ui/src/notifications/collab_notification.rs index 97b7100106..8efe2c5bb5 100644 --- a/crates/collab_ui/src/notifications/collab_notification.rs +++ b/crates/collab_ui/src/notifications/collab_notification.rs @@ -34,7 +34,7 @@ impl ParentElement for CollabNotification { impl RenderOnce for CollabNotification { fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() - .text_ui() + .text_ui(cx) .justify_between() .size_full() .overflow_hidden() diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 63aa1cc991..63a49e6220 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -306,7 +306,7 @@ impl PickerDelegate for OutlineViewDelegate { .selected(selected) .child( div() - .text_ui() + .text_ui(cx) .pl(rems(outline_item.depth as f32)) .child(styled_text), ), diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 94173304fc..880d3a3a47 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -160,7 +160,7 @@ impl RenderOnce for Key { } }) .h(rems_from_px(14.)) - .text_ui() + .text_ui(cx) .line_height(relative(1.)) .text_color(cx.theme().colors().text_muted) .child(self.key.clone()) diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 2d4577f05c..a7a90eecf0 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -108,10 +108,10 @@ impl RenderOnce for LabelLike { ) }) .map(|this| match self.size { - LabelSize::Large => this.text_ui_lg(), - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - LabelSize::XSmall => this.text_ui_xs(), + LabelSize::Large => this.text_ui_lg(cx), + LabelSize::Default => this.text_ui(cx), + LabelSize::Small => this.text_ui_sm(cx), + LabelSize::XSmall => this.text_ui_xs(cx), }) .when(self.line_height_style == LineHeightStyle::UiLabel, |this| { this.line_height(relative(1.)) diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 5d07f6b341..429def6f01 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -96,7 +96,7 @@ pub fn tooltip_container( v_flex() .elevation_2(cx) .font_family(ui_font) - .text_ui() + .text_ui(cx) .text_color(cx.theme().colors().text) .py_1() .px_2() diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 9a0f0ed1d2..1e1c28cc59 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -11,7 +11,7 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; -pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle}; +pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography}; pub use crate::visible_on_hover::*; pub use crate::{h_flex, v_flex}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index 70c14d1051..513742f8ee 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -1,9 +1,7 @@ use gpui::{hsla, px, Styled, WindowContext}; -use settings::Settings; -use theme::ThemeSettings; use crate::prelude::*; -use crate::{ElevationIndex, UiTextSize}; +use crate::ElevationIndex; fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) @@ -29,66 +27,6 @@ pub trait StyledExt: Styled + Sized { self.flex().flex_col() } - /// Sets the text size using a [`UiTextSize`]. - fn text_ui_size(self, size: UiTextSize) -> Self { - self.text_size(size.rems()) - } - - /// The large size for UI text. - /// - /// `1rem` or `16px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_lg(self) -> Self { - self.text_size(UiTextSize::Large.rems()) - } - - /// The default size for UI text. - /// - /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui_sm` for smaller text. - fn text_ui(self) -> Self { - self.text_size(UiTextSize::default().rems()) - } - - /// The small size for UI text. - /// - /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_sm(self) -> Self { - self.text_size(UiTextSize::Small.rems()) - } - - /// The extra small size for UI text. - /// - /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_xs(self) -> Self { - self.text_size(UiTextSize::XSmall.rems()) - } - - /// The font size for buffer text. - /// - /// Retrieves the default font size, or the user's custom font size if set. - /// - /// This should only be used for text that is displayed in a buffer, - /// or other places that text needs to match the user's buffer font size. - fn text_buffer(self, cx: &mut WindowContext) -> Self { - let settings = ThemeSettings::get_global(cx); - self.text_size(settings.buffer_font_size(cx)) - } - /// The [`Surface`](ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements /// /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index cd40cb1e99..8de54872cc 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -6,8 +6,73 @@ use theme::{ActiveTheme, ThemeSettings}; use crate::rems_from_px; +/// Extends [`gpui::Styled`] with typography-related styling methods. +pub trait StyledTypography: Styled + Sized { + /// Sets the text size using a [`UiTextSize`]. + fn text_ui_size(self, size: TextSize, cx: &WindowContext) -> Self { + self.text_size(size.rems(cx)) + } + + /// The large size for UI text. + /// + /// `1rem` or `16px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui` for regular-sized text. + fn text_ui_lg(self, cx: &WindowContext) -> Self { + self.text_size(TextSize::Large.rems(cx)) + } + + /// The default size for UI text. + /// + /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui_sm` for smaller text. + fn text_ui(self, cx: &WindowContext) -> Self { + self.text_size(TextSize::default().rems(cx)) + } + + /// The small size for UI text. + /// + /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui` for regular-sized text. + fn text_ui_sm(self, cx: &WindowContext) -> Self { + self.text_size(TextSize::Small.rems(cx)) + } + + /// The extra small size for UI text. + /// + /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. + /// + /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. + /// + /// Use `text_ui` for regular-sized text. + fn text_ui_xs(self, cx: &WindowContext) -> Self { + self.text_size(TextSize::XSmall.rems(cx)) + } + + /// The font size for buffer text. + /// + /// Retrieves the default font size, or the user's custom font size if set. + /// + /// This should only be used for text that is displayed in a buffer, + /// or other places that text needs to match the user's buffer font size. + fn text_buffer(self, cx: &mut WindowContext) -> Self { + let settings = ThemeSettings::get_global(cx); + self.text_size(settings.buffer_font_size(cx)) + } +} + +impl StyledTypography for E {} + #[derive(Debug, Default, Clone)] -pub enum UiTextSize { +pub enum TextSize { /// The default size for UI text. /// /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. @@ -35,15 +100,28 @@ pub enum UiTextSize { /// /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. XSmall, + + /// The `ui_font_size` set by the user. + UI, + /// The `buffer_font_size` set by the user. + Editor, + // TODO: The terminal settings will need to be passed to + // ThemeSettings before we can enable this. + //// The `terminal.font_size` set by the user. + // Terminal, } -impl UiTextSize { - pub fn rems(self) -> Rems { +impl TextSize { + pub fn rems(self, cx: &WindowContext) -> Rems { + let theme_settings = ThemeSettings::get_global(cx); + match self { Self::Large => rems_from_px(16.), Self::Default => rems_from_px(14.), Self::Small => rems_from_px(12.), Self::XSmall => rems_from_px(10.), + Self::UI => rems_from_px(theme_settings.ui_font_size.into()), + Self::Editor => rems_from_px(theme_settings.buffer_font_size.into()), } } } From cf2272a949b4b30fd954e511c12257e7cee96671 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 25 Apr 2024 16:34:07 -0700 Subject: [PATCH 075/101] Always submit function definitions in Simple mode too (#11016) Switches Assistant2 to always provide functions. It's up to the model to choose to use them. At a later point, the `Submit::Codebase` should change the `tool_choice` to `query_codebase` rather than `auto`. For now, I think this will improve the experience for folks testing. Release Notes: - N/A --- crates/assistant2/src/assistant2.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/assistant2/src/assistant2.rs b/crates/assistant2/src/assistant2.rs index 22d05a3fc5..8204dc3654 100644 --- a/crates/assistant2/src/assistant2.rs +++ b/crates/assistant2/src/assistant2.rs @@ -314,7 +314,8 @@ impl AssistantChat { let completion = this.update(cx, |this, cx| { this.push_new_assistant_message(cx); - let definitions = if call_count < limit && matches!(mode, SubmitMode::Codebase) + let definitions = if call_count < limit + && matches!(mode, SubmitMode::Codebase | SubmitMode::Simple) { this.tool_registry.definitions() } else { From 40fe5275cf7343596ca0130365529f3581f84944 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 25 Apr 2024 18:12:15 -0700 Subject: [PATCH 076/101] Rework project diagnostics to prevent showing inconsistent state (#10922) For a long time, we've had problems where diagnostics can end up showing up inconsistently in different views. This PR is my attempt to prevent that, and to simplify the system in the process. There are some UX changes. Diagnostic behaviors that have *not* changed: * In-buffer diagnostics update immediately when LSPs send diagnostics updates. * The diagnostic counts in the status bar indicator also update immediately. Diagnostic behaviors that this PR changes: * [x] The tab title for the project diagnostics view now simply shows the same counts as the status bar indicator - the project's current totals. Previously, this tab title showed something slightly different - the numbers of diagnostics *currently shown* in the diagnostics view's excerpts. But it was pretty confusing that you could sometimes see two different diagnostic counts. * [x] The project diagnostics view **never** updates its excerpts while the user might be in the middle of typing it that view, unless the user expressed an intent for the excerpts to update (by e.g. saving the buffer). This was the behavior we originally implemented, but has changed a few times since then, in attempts to fix other issues. I've restored that invariant. Times when the excerpts will update: * diagnostics are updated while the diagnostics view is not focused * the user changes focus away from the diagnostics view * the language server sends a `work done progress end` message for its disk-based diagnostics token (i.e. cargo check finishes) * the user saves a buffer associated with a language server, and then a debounce timer expires * [x] The project diagnostics view indicates when its diagnostics are stale. States: * when diagnostics have been updated while the diagnostics view was focused: * the indicator shows a 'refresh' icon * clicking the indicator updates the excerpts * when diagnostics have been updated, but a file has been saved, so that the diagnostics will soon update, the indicator is disabled With these UX changes, the only 'complex' part of the our diagnostics presentation is the Project Diagnostics view's excerpt management, because it needs to implement the deferred updates in order to avoid disrupting the user while they may be typing. I want to take some steps to reduce the potential for bugs in this view. * [x] Reduce the amount of state that the view uses, and simplify its implementation * [x] Add a randomized test that checks the invariant that a mutated diagnostics view matches a freshly computed diagnostics view ## Release Notes - Reworked the project diagnostics view: - Fixed an issue where the project diagnostics view could update its excerpts while you were typing in it. - Fixed bugs where the project diagnostics view could show the wrong excerpts. - Changed the diagnostics view to always update its excerpts eagerly when not focused. - Added an indicator to the project diagnostics view's toolbar, showing when diagnostics have been changed. --------- Co-authored-by: Richard Feldman --- Cargo.lock | 4 + crates/diagnostics/Cargo.toml | 4 + crates/diagnostics/src/diagnostics.rs | 1110 +++---------------- crates/diagnostics/src/diagnostics_tests.rs | 1008 +++++++++++++++++ crates/diagnostics/src/items.rs | 30 +- crates/diagnostics/src/toolbar_controls.rs | 67 +- crates/fs/src/fs.rs | 9 + crates/project/src/project.rs | 181 +-- 8 files changed, 1355 insertions(+), 1058 deletions(-) create mode 100644 crates/diagnostics/src/diagnostics_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 581375fae7..4c206e28c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3181,13 +3181,17 @@ dependencies = [ "anyhow", "client", "collections", + "ctor", "editor", + "env_logger", "futures 0.3.28", "gpui", "language", "log", "lsp", + "pretty_assertions", "project", + "rand 0.8.5", "schemars", "serde", "serde_json", diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index fcaacfd62a..48f05444e4 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -15,13 +15,16 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true +ctor.workspace = true editor.workspace = true +env_logger.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true lsp.workspace = true project.workspace = true +rand.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true @@ -40,3 +43,4 @@ serde_json.workspace = true theme = { workspace = true, features = ["test-support"] } unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 685c37ac60..b59e819db2 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -2,8 +2,11 @@ pub mod items; mod project_diagnostics_settings; mod toolbar_controls; -use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet}; +#[cfg(test)] +mod diagnostics_tests; + +use anyhow::Result; +use collections::{BTreeSet, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, @@ -11,7 +14,10 @@ use editor::{ scroll::Autoscroll, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; -use futures::future::try_join_all; +use futures::{ + channel::mpsc::{self, UnboundedSender}, + StreamExt as _, +}; use gpui::{ actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render, @@ -19,8 +25,7 @@ use gpui::{ WeakView, WindowContext, }; use language::{ - Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, - SelectionGoal, + Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, }; use lsp::LanguageServerId; use project::{DiagnosticSummary, Project, ProjectPath}; @@ -36,7 +41,7 @@ use std::{ use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; use ui::{h_flex, prelude::*, Icon, IconName, Label}; -use util::TryFutureExt; +use util::ResultExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, @@ -58,11 +63,12 @@ struct ProjectDiagnosticsEditor { summary: DiagnosticSummary, excerpts: Model, path_states: Vec, - paths_to_update: HashMap>, - current_diagnostics: HashMap>, + paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>, include_warnings: bool, context: u32, - _subscriptions: Vec, + update_paths_tx: UnboundedSender<(ProjectPath, Option)>, + _update_excerpts_task: Task>, + _subscription: Subscription, } struct PathState { @@ -70,13 +76,6 @@ struct PathState { diagnostic_groups: Vec, } -#[derive(Clone, Debug, PartialEq)] -struct Jump { - path: ProjectPath, - position: Point, - anchor: Anchor, -} - struct DiagnosticGroupState { language_server_id: LanguageServerId, primary_diagnostic: DiagnosticEntry, @@ -122,40 +121,38 @@ impl ProjectDiagnosticsEditor { cx: &mut ViewContext, ) -> Self { let project_event_subscription = - cx.subscribe(&project_handle, |this, _, event, cx| match event { + cx.subscribe(&project_handle, |this, project, event, cx| match event { + project::Event::DiskBasedDiagnosticsStarted { .. } => { + cx.notify(); + } project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { - log::debug!("Disk based diagnostics finished for server {language_server_id}"); - this.update_excerpts(Some(*language_server_id), cx); + log::debug!("disk based diagnostics finished for server {language_server_id}"); + this.enqueue_update_stale_excerpts(Some(*language_server_id)); } project::Event::DiagnosticsUpdated { language_server_id, path, } => { - log::debug!("Adding path {path:?} to update for server {language_server_id}"); this.paths_to_update - .entry(*language_server_id) - .or_default() - .insert(path.clone()); + .insert((path.clone(), *language_server_id)); + this.summary = project.read(cx).diagnostic_summary(false, cx); + cx.emit(EditorEvent::TitleChanged); - if this.is_dirty(cx) { - return; - } - let selections = this.editor.read(cx).selections.all::(cx); - if selections.len() < 2 - && selections - .first() - .map_or(true, |selection| selection.end == selection.start) - { - this.update_excerpts(Some(*language_server_id), cx); + if this.editor.read(cx).is_focused(cx) || this.focus_handle.is_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.enqueue_update_stale_excerpts(Some(*language_server_id)); } } _ => {} }); let focus_handle = cx.focus_handle(); - - let focus_in_subscription = - cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx)); + cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx)) + .detach(); + cx.on_focus_out(&focus_handle, |this, cx| this.focus_out(cx)) + .detach(); let excerpts = cx.new_model(|cx| { MultiBuffer::new( @@ -169,35 +166,52 @@ impl ProjectDiagnosticsEditor { editor.set_vertical_scroll_margin(5, cx); editor }); - let editor_event_subscription = - cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - cx.emit(event.clone()); - if event == &EditorEvent::Focused && this.path_states.is_empty() { - cx.focus(&this.focus_handle); + cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { + cx.emit(event.clone()); + match event { + EditorEvent::Focused => { + if this.path_states.is_empty() { + cx.focus(&this.focus_handle); + } } - }); + EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None), + _ => {} + } + }) + .detach(); + + let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded(); let project = project_handle.read(cx); - let summary = project.diagnostic_summary(false, cx); let mut this = Self { - project: project_handle, + project: project_handle.clone(), context, - summary, + summary: project.diagnostic_summary(false, cx), workspace, excerpts, focus_handle, editor, path_states: Default::default(), - paths_to_update: HashMap::default(), + paths_to_update: Default::default(), include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, - current_diagnostics: HashMap::default(), - _subscriptions: vec![ - project_event_subscription, - editor_event_subscription, - focus_in_subscription, - ], + update_paths_tx: update_excerpts_tx, + _update_excerpts_task: cx.spawn(move |this, mut cx| async move { + while let Some((path, language_server_id)) = update_excerpts_rx.next().await { + if let Some(buffer) = project_handle + .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))? + .await + .log_err() + { + this.update(&mut cx, |this, cx| { + this.update_excerpts(path, language_server_id, buffer, cx); + })?; + } + } + anyhow::Ok(()) + }), + _subscription: project_event_subscription, }; - this.update_excerpts(None, cx); + this.enqueue_update_all_excerpts(cx); this } @@ -228,8 +242,7 @@ impl ProjectDiagnosticsEditor { fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { self.include_warnings = !self.include_warnings; - self.paths_to_update = self.current_diagnostics.clone(); - self.update_excerpts(None, cx); + self.enqueue_update_all_excerpts(cx); cx.notify(); } @@ -239,122 +252,65 @@ impl ProjectDiagnosticsEditor { } } - fn update_excerpts( - &mut self, - language_server_id: Option, - cx: &mut ViewContext, - ) { - log::debug!("Updating excerpts for server {language_server_id:?}"); - let mut paths_to_recheck = HashSet::default(); - let mut new_summaries: HashMap> = self - .project - .read(cx) - .diagnostic_summaries(false, cx) - .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { - summaries.entry(server_id).or_default().insert(path); - summaries - }); - let mut old_diagnostics = if let Some(language_server_id) = language_server_id { - new_summaries.retain(|server_id, _| server_id == &language_server_id); - self.paths_to_update.retain(|server_id, paths| { - if server_id == &language_server_id { - paths_to_recheck.extend(paths.drain()); - false - } else { - true - } - }); - let mut old_diagnostics = HashMap::default(); - if let Some(new_paths) = new_summaries.get(&language_server_id) { - if let Some(old_paths) = self - .current_diagnostics - .insert(language_server_id, new_paths.clone()) - { - old_diagnostics.insert(language_server_id, old_paths); - } - } else { - if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) { - old_diagnostics.insert(language_server_id, old_paths); - } - } - old_diagnostics - } else { - paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths)); - mem::replace(&mut self.current_diagnostics, new_summaries.clone()) - }; - for (server_id, new_paths) in new_summaries { - match old_diagnostics.remove(&server_id) { - Some(mut old_paths) => { - paths_to_recheck.extend( - new_paths - .into_iter() - .filter(|new_path| !old_paths.remove(new_path)), - ); - paths_to_recheck.extend(old_paths); - } - None => paths_to_recheck.extend(new_paths), - } + fn focus_out(&mut self, cx: &mut ViewContext) { + if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) { + self.enqueue_update_stale_excerpts(None); } - paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths)); - - if paths_to_recheck.is_empty() { - log::debug!("No paths to recheck for language server {language_server_id:?}"); - return; - } - log::debug!( - "Rechecking {} paths for language server {:?}", - paths_to_recheck.len(), - language_server_id - ); - let project = self.project.clone(); - cx.spawn(|this, mut cx| { - async move { - let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| { - let mut cx = cx.clone(); - let project = project.clone(); - let this = this.clone(); - async move { - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))? - .await - .with_context(|| format!("opening buffer for path {path:?}"))?; - this.update(&mut cx, |this, cx| { - this.populate_excerpts(path, language_server_id, buffer, cx); - }) - .context("missing project")?; - anyhow::Ok(()) - } - })) - .await - .context("rechecking diagnostics for paths")?; - - this.update(&mut cx, |this, cx| { - this.summary = this.project.read(cx).diagnostic_summary(false, cx); - cx.emit(EditorEvent::TitleChanged); - })?; - anyhow::Ok(()) - } - .log_err() - }) - .detach(); } - fn populate_excerpts( + /// Enqueue an update of all excerpts. Updates all paths that either + /// currently have diagnostics or are currently present in this view. + fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext) { + self.project.update(cx, |project, cx| { + let mut paths = project + .diagnostic_summaries(false, cx) + .map(|(path, _, _)| path) + .collect::>(); + paths.extend(self.path_states.iter().map(|state| state.path.clone())); + for path in paths { + self.update_paths_tx.unbounded_send((path, None)).unwrap(); + } + }); + } + + /// Enqueue an update of the excerpts for any path whose diagnostics are known + /// to have changed. If a language server id is passed, then only the excerpts for + /// that language server's diagnostics will be updated. Otherwise, all stale excerpts + /// will be refreshed. + fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option) { + for (path, server_id) in &self.paths_to_update { + if language_server_id.map_or(true, |id| id == *server_id) { + self.update_paths_tx + .unbounded_send((path.clone(), Some(*server_id))) + .unwrap(); + } + } + } + + fn update_excerpts( &mut self, - path: ProjectPath, - language_server_id: Option, + path_to_update: ProjectPath, + server_to_update: Option, buffer: Model, cx: &mut ViewContext, ) { + self.paths_to_update.retain(|(path, server_id)| { + *path != path_to_update + || server_to_update.map_or(false, |to_update| *server_id != to_update) + }); + let was_empty = self.path_states.is_empty(); let snapshot = buffer.read(cx).snapshot(); - let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) { + let path_ix = match self + .path_states + .binary_search_by_key(&&path_to_update, |e| &e.path) + { Ok(ix) => ix, Err(ix) => { self.path_states.insert( ix, PathState { - path: path.clone(), + path: path_to_update.clone(), diagnostic_groups: Default::default(), }, ); @@ -373,8 +329,7 @@ impl ProjectDiagnosticsEditor { }; let path_state = &mut self.path_states[path_ix]; - let mut groups_to_add = Vec::new(); - let mut group_ixs_to_remove = Vec::new(); + 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; @@ -383,10 +338,13 @@ impl ProjectDiagnosticsEditor { } else { DiagnosticSeverity::ERROR }; - let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| { - let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable(); + let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| { + let mut old_groups = mem::take(&mut path_state.diagnostic_groups) + .into_iter() + .enumerate() + .peekable(); let mut new_groups = snapshot - .diagnostic_groups(language_server_id) + .diagnostic_groups(server_to_update) .into_iter() .filter(|(_, group)| { group.entries[group.primary_ix].diagnostic.severity <= max_severity @@ -400,19 +358,20 @@ impl ProjectDiagnosticsEditor { (None, None) => break, (None, Some(_)) => to_insert = new_groups.next(), (Some((_, old_group)), None) => { - if language_server_id.map_or(true, |id| id == old_group.language_server_id) - { + if server_to_update.map_or(true, |id| id == old_group.language_server_id) { to_remove = old_groups.next(); } else { to_keep = old_groups.next(); } } - (Some((_, old_group)), Some((_, new_group))) => { + (Some((_, old_group)), Some((new_language_server_id, new_group))) => { let old_primary = &old_group.primary_diagnostic; let new_primary = &new_group.entries[new_group.primary_ix]; - match compare_diagnostics(old_primary, new_primary, &snapshot) { + match compare_diagnostics(old_primary, new_primary, &snapshot) + .then_with(|| old_group.language_server_id.cmp(new_language_server_id)) + { Ordering::Less => { - if language_server_id + if server_to_update .map_or(true, |id| id == old_group.language_server_id) { to_remove = old_groups.next(); @@ -456,6 +415,7 @@ impl ProjectDiagnosticsEditor { Point::new(range.end.row + self.context, u32::MAX), Bias::Left, ); + let excerpt_id = excerpts .insert_excerpts_after( prev_excerpt_id, @@ -464,7 +424,7 @@ impl ProjectDiagnosticsEditor { context: excerpt_start..excerpt_end, primary: Some(range.clone()), }], - excerpts_cx, + cx, ) .pop() .unwrap(); @@ -518,18 +478,19 @@ impl ProjectDiagnosticsEditor { } } - groups_to_add.push(group_state); - } else if let Some((group_ix, group_state)) = to_remove { - excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx); - group_ixs_to_remove.push(group_ix); + new_group_ixs.push(path_state.diagnostic_groups.len()); + path_state.diagnostic_groups.push(group_state); + } else if let Some((_, group_state)) = to_remove { + excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx); blocks_to_remove.extend(group_state.blocks.iter().copied()); - } else if let Some((_, group)) = to_keep { - prev_excerpt_id = *group.excerpts.last().unwrap(); + } else if let Some((_, group_state)) = to_keep { + prev_excerpt_id = *group_state.excerpts.last().unwrap(); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id); + path_state.diagnostic_groups.push(group_state); } } - excerpts.snapshot(excerpts_cx) + excerpts.snapshot(cx) }); self.editor.update(cx, |editor, cx| { @@ -550,24 +511,12 @@ impl ProjectDiagnosticsEditor { ); let mut block_ids = block_ids.into_iter(); - for group_state in &mut groups_to_add { + for ix in new_group_ixs { + let group_state = &mut path_state.diagnostic_groups[ix]; group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); } }); - for ix in group_ixs_to_remove.into_iter().rev() { - path_state.diagnostic_groups.remove(ix); - } - path_state.diagnostic_groups.extend(groups_to_add); - path_state.diagnostic_groups.sort_unstable_by(|a, b| { - let range_a = &a.primary_diagnostic.range; - let range_b = &b.primary_diagnostic.range; - range_a - .start - .cmp(&range_b.start, &snapshot) - .then_with(|| range_a.end.cmp(&range_b.end, &snapshot)) - }); - if path_state.diagnostic_groups.is_empty() { self.path_states.remove(path_ix); } @@ -634,8 +583,32 @@ impl ProjectDiagnosticsEditor { let focus_handle = self.editor.focus_handle(cx); cx.focus(&focus_handle); } + + #[cfg(test)] + self.check_invariants(cx); + cx.notify(); } + + #[cfg(test)] + fn check_invariants(&self, cx: &mut ViewContext) { + let mut excerpts = Vec::new(); + for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() { + if let Some(file) = buffer.file() { + excerpts.push((id, file.path().clone())); + } + } + + let mut prev_path = None; + for (_, path) in &excerpts { + if let Some(prev_path) = prev_path { + if path < prev_path { + panic!("excerpts are not sorted by path {:?}", excerpts); + } + } + prev_path = Some(path); + } + } } impl FocusableView for ProjectDiagnosticsEditor { @@ -904,762 +877,3 @@ fn compare_diagnostics( }) .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message)) } - -#[cfg(test)] -mod tests { - use super::*; - use editor::{ - display_map::{BlockContext, TransformBlock}, - DisplayPoint, GutterDimensions, - }; - use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext}; - use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; - use project::FakeFs; - use serde_json::json; - use settings::SettingsStore; - use unindent::Unindent as _; - - #[gpui::test] - async fn test_diagnostics(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "consts.rs": " - const a: i32 = 'a'; - const b: i32 = c; - " - .unindent(), - - "main.rs": " - fn main() { - let x = vec![]; - let y = vec![]; - a(x); - b(y); - // comment 1 - // comment 2 - c(y); - d(x); - } - " - .unindent(), - }), - ) - .await; - - let language_server_id = LanguageServerId(0); - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); - - // Create some diagnostics - project.update(cx, |project, cx| { - project - .update_diagnostic_entries( - language_server_id, - PathBuf::from("/test/main.rs"), - None, - vec![ - DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)), - diagnostic: Diagnostic { - message: - "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" - .to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)), - diagnostic: Diagnostic { - message: - "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" - .to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)), - diagnostic: Diagnostic { - message: "value moved here".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)), - diagnostic: Diagnostic { - message: "value moved here".to_string(), - severity: DiagnosticSeverity::INFORMATION, - is_primary: false, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)), - diagnostic: Diagnostic { - message: "use of moved value\nvalue used here after move".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)), - diagnostic: Diagnostic { - message: "use of moved value\nvalue used here after move".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - ], - cx, - ) - .unwrap(); - }); - - // 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, - ) - }); - let editor = view.update(cx, |view, _| view.editor.clone()); - - view.next_notification(cx).await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (15, "collapsed context".into()), - (16, "diagnostic header".into()), - (25, "collapsed context".into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); - - // Cursor is at the first diagnostic - editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.display_ranges(cx), - [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)] - ); - }); - - // Diagnostics are added for another earlier path. - project.update(cx, |project, cx| { - project.disk_based_diagnostics_started(language_server_id, cx); - project - .update_diagnostic_entries( - language_server_id, - PathBuf::from("/test/consts.rs"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), - diagnostic: Diagnostic { - message: "mismatched types\nexpected `usize`, found `char`".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); - project.disk_based_diagnostics_finished(language_server_id, cx); - }); - - view.next_notification(cx).await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "path header block".into()), - (9, "diagnostic header".into()), - (22, "collapsed context".into()), - (23, "diagnostic header".into()), - (32, "collapsed context".into()), - ] - ); - - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); - - // Cursor keeps its position. - editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.display_ranges(cx), - [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)] - ); - }); - - // Diagnostics are added to the first path - project.update(cx, |project, cx| { - project.disk_based_diagnostics_started(language_server_id, cx); - project - .update_diagnostic_entries( - language_server_id, - PathBuf::from("/test/consts.rs"), - None, - vec![ - DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 15)) - ..Unclipped(PointUtf16::new(0, 15)), - diagnostic: Diagnostic { - message: "mismatched types\nexpected `usize`, found `char`" - .to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 0, - ..Default::default() - }, - }, - DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 15)) - ..Unclipped(PointUtf16::new(1, 15)), - diagnostic: Diagnostic { - message: "unresolved name `c`".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }, - ], - cx, - ) - .unwrap(); - project.disk_based_diagnostics_finished(language_server_id, cx); - }); - - view.next_notification(cx).await; - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - (13, "path header block".into()), - (15, "diagnostic header".into()), - (28, "collapsed context".into()), - (29, "diagnostic header".into()), - (38, "collapsed context".into()), - ] - ); - - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - // - // consts.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "\n", // supporting diagnostic - "const b: i32 = c;\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "const a: i32 = 'a';\n", - "const b: i32 = c;\n", - "\n", // supporting diagnostic - // - // main.rs - // - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - " let x = vec![];\n", - " let y = vec![];\n", - "\n", // supporting diagnostic - " a(x);\n", - " b(y);\n", - "\n", // supporting diagnostic - " // comment 1\n", - " // comment 2\n", - " c(y);\n", - "\n", // supporting diagnostic - " d(x);\n", - "\n", // context ellipsis - // diagnostic group 2 - "\n", // primary message - "\n", // filename - "fn main() {\n", - " let x = vec![];\n", - "\n", // supporting diagnostic - " let y = vec![];\n", - " a(x);\n", - "\n", // supporting diagnostic - " b(y);\n", - "\n", // context ellipsis - " c(y);\n", - " d(x);\n", - "\n", // supporting diagnostic - "}" - ) - ); - } - - #[gpui::test] - async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "main.js": " - a(); - b(); - c(); - d(); - e(); - ".unindent() - }), - ) - .await; - - let server_id_1 = LanguageServerId(100); - let server_id_2 = LanguageServerId(101); - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*window, cx); - let workspace = window.root(cx).unwrap(); - - let view = window.build_view(cx, |cx| { - ProjectDiagnosticsEditor::new_with_context( - 1, - project.clone(), - workspace.downgrade(), - cx, - ) - }); - let editor = view.update(cx, |view, _| view.editor.clone()); - - // Two language servers start updating diagnostics - project.update(cx, |project, cx| { - project.disk_based_diagnostics_started(server_id_1, cx); - project.disk_based_diagnostics_started(server_id_2, cx); - project - .update_diagnostic_entries( - server_id_1, - PathBuf::from("/test/main.js"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)), - diagnostic: Diagnostic { - message: "error 1".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); - }); - - // The first language server finishes - project.update(cx, |project, cx| { - project.disk_based_diagnostics_finished(server_id_1, cx); - }); - - // Only the first language server's diagnostics are shown. - cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // - "b();", - ) - ); - - // The second language server finishes - project.update(cx, |project, cx| { - project - .update_diagnostic_entries( - server_id_2, - PathBuf::from("/test/main.js"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), - diagnostic: Diagnostic { - message: "warning 1".to_string(), - severity: DiagnosticSeverity::ERROR, - is_primary: true, - is_disk_based: true, - group_id: 2, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); - project.disk_based_diagnostics_finished(server_id_2, cx); - }); - - // Both language server's diagnostics are shown. - cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (6, "collapsed context".into()), - (7, "diagnostic header".into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "a();\n", // context - "b();\n", // - "c();", // context - ) - ); - - // Both language servers start updating diagnostics, and the first server finishes. - project.update(cx, |project, cx| { - project.disk_based_diagnostics_started(server_id_1, cx); - project.disk_based_diagnostics_started(server_id_2, cx); - project - .update_diagnostic_entries( - server_id_1, - PathBuf::from("/test/main.js"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)), - diagnostic: Diagnostic { - message: "warning 2".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); - project - .update_diagnostic_entries( - server_id_2, - PathBuf::from("/test/main.rs"), - None, - vec![], - cx, - ) - .unwrap(); - project.disk_based_diagnostics_finished(server_id_1, cx); - }); - - // Only the first language server's diagnostics are updated. - cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "a();\n", // location - "b();\n", // - "c();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "b();\n", // context - "c();\n", // - "d();", // context - ) - ); - - // The second language server finishes. - project.update(cx, |project, cx| { - project - .update_diagnostic_entries( - server_id_2, - PathBuf::from("/test/main.js"), - None, - vec![DiagnosticEntry { - range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)), - diagnostic: Diagnostic { - message: "warning 2".to_string(), - severity: DiagnosticSeverity::WARNING, - is_primary: true, - is_disk_based: true, - group_id: 1, - ..Default::default() - }, - }], - cx, - ) - .unwrap(); - project.disk_based_diagnostics_finished(server_id_2, cx); - }); - - // Both language servers' diagnostics are updated. - cx.executor().run_until_parked(); - assert_eq!( - editor_blocks(&editor, cx), - [ - (0, "path header block".into()), - (2, "diagnostic header".into()), - (7, "collapsed context".into()), - (8, "diagnostic header".into()), - ] - ); - assert_eq!( - editor.update(cx, |editor, cx| editor.display_text(cx)), - concat!( - "\n", // filename - "\n", // padding - // diagnostic group 1 - "\n", // primary message - "\n", // padding - "b();\n", // location - "c();\n", // - "d();\n", // context - "\n", // collapsed context - // diagnostic group 2 - "\n", // primary message - "\n", // padding - "c();\n", // context - "d();\n", // - "e();", // context - ) - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - theme::init(theme::LoadThemes::JustBase, cx); - language::init(cx); - client::init_settings(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - crate::init(cx); - editor::init(cx); - }); - } - - fn editor_blocks( - editor: &View, - cx: &mut VisualTestContext, - ) -> Vec<(u32, SharedString)> { - let mut blocks = Vec::new(); - cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - blocks.extend( - snapshot - .blocks_in_range(0..snapshot.max_point().row()) - .enumerate() - .filter_map(|(ix, (row, block))| { - let name: SharedString = match block { - TransformBlock::Custom(block) => { - let mut element = block.render(&mut BlockContext { - context: cx, - anchor_x: px(0.), - gutter_dimensions: &GutterDimensions::default(), - line_height: px(0.), - em_width: px(0.), - max_width: px(0.), - block_id: ix, - editor_style: &editor::EditorStyle::default(), - }); - let element = element.downcast_mut::>().unwrap(); - element - .interactivity() - .element_id - .clone()? - .try_into() - .ok()? - } - - TransformBlock::ExcerptHeader { - starts_new_buffer, .. - } => { - if *starts_new_buffer { - "path header block".into() - } else { - "collapsed context".into() - } - } - }; - - Some((row, name)) - }), - ) - }); - - div().into_any() - }); - blocks - } -} diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs new file mode 100644 index 0000000000..3e7b4b67f2 --- /dev/null +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -0,0 +1,1008 @@ +use super::*; +use collections::HashMap; +use editor::{ + display_map::{BlockContext, TransformBlock}, + DisplayPoint, GutterDimensions, +}; +use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext}; +use language::{ + Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, PointUtf16, Rope, Unclipped, +}; +use pretty_assertions::assert_eq; +use project::FakeFs; +use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng}; +use serde_json::json; +use settings::SettingsStore; +use std::{env, path::Path}; +use unindent::Unindent as _; +use util::{post_inc, RandomCharIter}; + +#[ctor::ctor] +fn init_logger() { + if env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +#[gpui::test] +async fn test_diagnostics(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + "consts.rs": " + const a: i32 = 'a'; + const b: i32 = c; + " + .unindent(), + + "main.rs": " + fn main() { + let x = vec![]; + let y = vec![]; + a(x); + b(y); + // comment 1 + // comment 2 + c(y); + d(x); + } + " + .unindent(), + }), + ) + .await; + + let language_server_id = LanguageServerId(0); + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + // Create some diagnostics + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/main.rs"), + None, + vec![ + DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)), + diagnostic: Diagnostic { + message: + "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" + .to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)), + diagnostic: Diagnostic { + message: + "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" + .to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)), + diagnostic: Diagnostic { + message: "value moved here".to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)), + diagnostic: Diagnostic { + message: "value moved here".to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)), + diagnostic: Diagnostic { + message: "use of moved value\nvalue used here after move".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)), + diagnostic: Diagnostic { + message: "use of moved value\nvalue used here after move".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + ], + cx, + ) + .unwrap(); + }); + + // 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) + }); + let editor = view.update(cx, |view, _| view.editor.clone()); + + view.next_notification(cx).await; + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (15, "collapsed context".into()), + (16, "diagnostic header".into()), + (25, "collapsed context".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + + // Cursor is at the first diagnostic + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)] + ); + }); + + // Diagnostics are added for another earlier path. + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(language_server_id, cx); + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/consts.rs"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), + diagnostic: Diagnostic { + message: "mismatched types\nexpected `usize`, found `char`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(language_server_id, cx); + }); + + view.next_notification(cx).await; + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "path header block".into()), + (9, "diagnostic header".into()), + (22, "collapsed context".into()), + (23, "diagnostic header".into()), + (32, "collapsed context".into()), + ] + ); + + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + + // Cursor keeps its position. + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)] + ); + }); + + // Diagnostics are added to the first path + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(language_server_id, cx); + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/consts.rs"), + None, + vec![ + DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), + diagnostic: Diagnostic { + message: "mismatched types\nexpected `usize`, found `char`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 15))..Unclipped(PointUtf16::new(1, 15)), + diagnostic: Diagnostic { + message: "unresolved name `c`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + ], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(language_server_id, cx); + }); + + view.next_notification(cx).await; + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + (13, "path header block".into()), + (15, "diagnostic header".into()), + (28, "collapsed context".into()), + (29, "diagnostic header".into()), + (38, "collapsed context".into()), + ] + ); + + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "const b: i32 = c;\n", + "\n", // supporting diagnostic + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); +} + +#[gpui::test] +async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + "main.js": " + a(); + b(); + c(); + d(); + e(); + ".unindent() + }), + ) + .await; + + let server_id_1 = LanguageServerId(100); + let server_id_2 = LanguageServerId(101); + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + let view = window.build_view(cx, |cx| { + ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + }); + let editor = view.update(cx, |view, _| view.editor.clone()); + + // Two language servers start updating diagnostics + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(server_id_1, cx); + project.disk_based_diagnostics_started(server_id_2, cx); + project + .update_diagnostic_entries( + server_id_1, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)), + diagnostic: Diagnostic { + message: "error 1".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + }); + + // The first language server finishes + project.update(cx, |project, cx| { + project.disk_based_diagnostics_finished(server_id_1, cx); + }); + + // Only the first language server's diagnostics are shown. + cx.executor().run_until_parked(); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // + "b();", + ) + ); + + // The second language server finishes + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), + diagnostic: Diagnostic { + message: "warning 1".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 2, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_2, cx); + }); + + // Both language server's diagnostics are shown. + cx.executor().run_until_parked(); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (6, "collapsed context".into()), + (7, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "a();\n", // context + "b();\n", // + "c();", // context + ) + ); + + // Both language servers start updating diagnostics, and the first server finishes. + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(server_id_1, cx); + project.disk_based_diagnostics_started(server_id_2, cx); + project + .update_diagnostic_entries( + server_id_1, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)), + diagnostic: Diagnostic { + message: "warning 2".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.rs"), + None, + vec![], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_1, cx); + }); + + // Only the first language server's diagnostics are updated. + cx.executor().run_until_parked(); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "c();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "b();\n", // context + "c();\n", // + "d();", // context + ) + ); + + // The second language server finishes. + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)), + diagnostic: Diagnostic { + message: "warning 2".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_2, cx); + }); + + // Both language servers' diagnostics are updated. + cx.executor().run_until_parked(); + assert_eq!( + editor_blocks(&editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "b();\n", // location + "c();\n", // + "d();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "c();\n", // context + "d();\n", // + "e();", // context + ) + ); +} + +#[gpui::test(iterations = 20)] +async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { + init_test(cx); + + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/test", json!({})).await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + let mutated_view = window.build_view(cx, |cx| { + ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + }); + + workspace.update(cx, |workspace, cx| { + workspace.add_item_to_center(Box::new(mutated_view.clone()), cx); + }); + mutated_view.update(cx, |view, cx| { + assert!(view.focus_handle.is_focused(cx)); + }); + + let mut next_group_id = 0; + let mut next_filename = 0; + let mut language_server_ids = vec![LanguageServerId(0)]; + let mut updated_language_servers = HashSet::default(); + let mut current_diagnostics: HashMap< + (PathBuf, LanguageServerId), + Vec>>, + > = Default::default(); + + for _ in 0..operations { + match rng.gen_range(0..100) { + // language server completes its diagnostic check + 0..=20 if !updated_language_servers.is_empty() => { + let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap(); + log::info!("finishing diagnostic check for language server {server_id}"); + project.update(cx, |project, cx| { + project.disk_based_diagnostics_finished(server_id, cx) + }); + + if rng.gen_bool(0.5) { + cx.run_until_parked(); + } + } + + // language server updates diagnostics + _ => { + let (path, server_id, diagnostics) = + match current_diagnostics.iter_mut().choose(&mut rng) { + // update existing set of diagnostics + Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => { + (path.clone(), *server_id, diagnostics) + } + + // insert a set of diagnostics for a new path + _ => { + let path: PathBuf = + format!("/test/{}.rs", post_inc(&mut next_filename)).into(); + let len = rng.gen_range(128..256); + let content = + RandomCharIter::new(&mut rng).take(len).collect::(); + fs.insert_file(&path, content.into_bytes()).await; + + let server_id = match language_server_ids.iter().choose(&mut rng) { + Some(server_id) if rng.gen_bool(0.5) => *server_id, + _ => { + let id = LanguageServerId(language_server_ids.len()); + language_server_ids.push(id); + id + } + }; + + ( + path.clone(), + server_id, + current_diagnostics + .entry((path, server_id)) + .or_insert(vec![]), + ) + } + }; + + updated_language_servers.insert(server_id); + + project.update(cx, |project, cx| { + log::info!("updating diagnostics. language server {server_id} path {path:?}"); + randomly_update_diagnostics_for_path( + &fs, + &path, + diagnostics, + &mut next_group_id, + &mut rng, + ); + project + .update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx) + .unwrap() + }); + + cx.run_until_parked(); + } + } + } + + log::info!("updating mutated diagnostics view"); + mutated_view.update(cx, |view, _| view.enqueue_update_stale_excerpts(None)); + cx.run_until_parked(); + + log::info!("constructing reference diagnostics view"); + let reference_view = window.build_view(cx, |cx| { + ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + }); + cx.run_until_parked(); + + let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx); + let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx); + assert_eq!(mutated_excerpts, reference_excerpts); +} + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + client::init_settings(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + crate::init(cx); + editor::init(cx); + }); +} + +#[derive(Debug, PartialEq, Eq)] +struct ExcerptInfo { + path: PathBuf, + range: ExcerptRange, + group_id: usize, + primary: bool, + language_server: LanguageServerId, +} + +fn get_diagnostics_excerpts( + view: &View, + cx: &mut VisualTestContext, +) -> Vec { + view.update(cx, |view, cx| { + let mut result = vec![]; + let mut excerpt_indices_by_id = HashMap::default(); + view.excerpts.update(cx, |multibuffer, cx| { + let snapshot = multibuffer.snapshot(cx); + for (id, buffer, range) in snapshot.excerpts() { + excerpt_indices_by_id.insert(id, result.len()); + result.push(ExcerptInfo { + path: buffer.file().unwrap().path().to_path_buf(), + range: ExcerptRange { + context: range.context.to_point(&buffer), + primary: range.primary.map(|range| range.to_point(&buffer)), + }, + group_id: usize::MAX, + primary: false, + language_server: LanguageServerId(0), + }); + } + }); + + for state in &view.path_states { + for group in &state.diagnostic_groups { + for (ix, excerpt_id) in group.excerpts.iter().enumerate() { + let excerpt_ix = excerpt_indices_by_id[excerpt_id]; + let excerpt = &mut result[excerpt_ix]; + excerpt.group_id = group.primary_diagnostic.diagnostic.group_id; + excerpt.language_server = group.language_server_id; + excerpt.primary = ix == group.primary_excerpt_ix; + } + } + } + + result + }) +} + +fn randomly_update_diagnostics_for_path( + fs: &FakeFs, + path: &Path, + diagnostics: &mut Vec>>, + next_group_id: &mut usize, + rng: &mut impl Rng, +) { + let file_content = fs.read_file_sync(path).unwrap(); + let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref()); + + let mut group_ids = diagnostics + .iter() + .map(|d| d.diagnostic.group_id) + .collect::>(); + + let mutation_count = rng.gen_range(1..=3); + for _ in 0..mutation_count { + if rng.gen_bool(0.5) && !group_ids.is_empty() { + let group_id = *group_ids.iter().choose(rng).unwrap(); + log::info!(" removing diagnostic group {group_id}"); + diagnostics.retain(|d| d.diagnostic.group_id != group_id); + group_ids.remove(&group_id); + } else { + let group_id = *next_group_id; + *next_group_id += 1; + + let mut new_diagnostics = vec![random_diagnostic(rng, &file_text, group_id, true)]; + for _ in 0..rng.gen_range(0..=1) { + new_diagnostics.push(random_diagnostic(rng, &file_text, group_id, false)); + } + + let ix = rng.gen_range(0..=diagnostics.len()); + log::info!( + " inserting diagnostic group {group_id} at index {ix}. ranges: {:?}", + new_diagnostics + .iter() + .map(|d| (d.range.start.0, d.range.end.0)) + .collect::>() + ); + diagnostics.splice(ix..ix, new_diagnostics); + } + } +} + +fn random_diagnostic( + rng: &mut impl Rng, + file_text: &Rope, + group_id: usize, + is_primary: bool, +) -> DiagnosticEntry> { + // Intentionally allow erroneous ranges some of the time (that run off the end of the file), + // because language servers can potentially give us those, and we should handle them gracefully. + const ERROR_MARGIN: usize = 10; + + let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN)); + let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN)); + let range = Range { + start: Unclipped(file_text.offset_to_point_utf16(start)), + end: Unclipped(file_text.offset_to_point_utf16(end)), + }; + let severity = if rng.gen_bool(0.5) { + DiagnosticSeverity::WARNING + } else { + DiagnosticSeverity::ERROR + }; + let message = format!("diagnostic group {group_id}"); + + DiagnosticEntry { + range, + diagnostic: Diagnostic { + source: None, // (optional) service that created the diagnostic + code: None, // (optional) machine-readable code that identifies the diagnostic + severity, + message, + group_id, + is_primary, + is_disk_based: false, + is_unnecessary: false, + }, + } +} + +fn editor_blocks(editor: &View, cx: &mut VisualTestContext) -> Vec<(u32, SharedString)> { + let mut blocks = Vec::new(); + cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + blocks.extend( + snapshot + .blocks_in_range(0..snapshot.max_point().row()) + .enumerate() + .filter_map(|(ix, (row, block))| { + let name: SharedString = match block { + TransformBlock::Custom(block) => { + let mut element = block.render(&mut BlockContext { + context: cx, + anchor_x: px(0.), + gutter_dimensions: &GutterDimensions::default(), + line_height: px(0.), + em_width: px(0.), + max_width: px(0.), + block_id: ix, + editor_style: &editor::EditorStyle::default(), + }); + let element = element.downcast_mut::>().unwrap(); + element + .interactivity() + .element_id + .clone()? + .try_into() + .ok()? + } + + TransformBlock::ExcerptHeader { + starts_new_buffer, .. + } => { + if *starts_new_buffer { + "path header block".into() + } else { + "collapsed context".into() + } + } + }; + + Some((row, name)) + }), + ) + }); + + div().into_any() + }); + blocks +} diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 03d46ed599..715da22ef1 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,13 +1,11 @@ use std::time::Duration; -use collections::HashSet; use editor::Editor; use gpui::{ percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Transformation, View, ViewContext, WeakView, }; use language::Diagnostic; -use lsp::LanguageServerId; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; @@ -18,7 +16,6 @@ pub struct DiagnosticIndicator { active_editor: Option>, workspace: WeakView, current_diagnostic: Option, - in_progress_checks: HashSet, _observe_active_editor: Option, } @@ -64,7 +61,20 @@ impl Render for DiagnosticIndicator { .child(Label::new(warning_count.to_string()).size(LabelSize::Small)), }; - let status = if !self.in_progress_checks.is_empty() { + let has_in_progress_checks = self + .workspace + .upgrade() + .and_then(|workspace| { + workspace + .read(cx) + .project() + .read(cx) + .language_servers_running_disk_based_diagnostics() + .next() + }) + .is_some(); + + let status = if has_in_progress_checks { Some( h_flex() .gap_2() @@ -126,15 +136,13 @@ impl DiagnosticIndicator { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { let project = workspace.project(); cx.subscribe(project, |this, project, event, cx| match event { - project::Event::DiskBasedDiagnosticsStarted { language_server_id } => { - this.in_progress_checks.insert(*language_server_id); + project::Event::DiskBasedDiagnosticsStarted { .. } => { cx.notify(); } - project::Event::DiskBasedDiagnosticsFinished { language_server_id } - | project::Event::LanguageServerRemoved(language_server_id) => { + project::Event::DiskBasedDiagnosticsFinished { .. } + | project::Event::LanguageServerRemoved(_) => { this.summary = project.read(cx).diagnostic_summary(false, cx); - this.in_progress_checks.remove(language_server_id); cx.notify(); } @@ -149,10 +157,6 @@ impl DiagnosticIndicator { Self { summary: project.read(cx).diagnostic_summary(false, cx), - in_progress_checks: project - .read(cx) - .language_servers_running_disk_based_diagnostics() - .collect(), active_editor: None, workspace: workspace.weak_handle(), current_diagnostic: None, diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 3c09e3fad9..7f4deba73e 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,5 +1,5 @@ use crate::ProjectDiagnosticsEditor; -use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; +use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView}; use ui::prelude::*; use ui::{IconButton, IconName, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; @@ -10,12 +10,23 @@ pub struct ToolbarControls { impl Render for ToolbarControls { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let include_warnings = self - .editor - .as_ref() - .and_then(|editor| editor.upgrade()) - .map(|editor| editor.read(cx).include_warnings) - .unwrap_or(false); + let mut include_warnings = false; + let mut has_stale_excerpts = false; + let mut is_updating = false; + + if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) { + let editor = editor.read(cx); + + include_warnings = editor.include_warnings; + has_stale_excerpts = !editor.paths_to_update.is_empty(); + is_updating = editor.update_paths_tx.len() > 0 + || editor + .project + .read(cx) + .language_servers_running_disk_based_diagnostics() + .next() + .is_some(); + } let tooltip = if include_warnings { "Exclude Warnings" @@ -23,17 +34,37 @@ impl Render for ToolbarControls { "Include Warnings" }; - div().child( - IconButton::new("toggle-warnings", IconName::ExclamationTriangle) - .tooltip(move |cx| Tooltip::text(tooltip, cx)) - .on_click(cx.listener(|this, _, cx| { - if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) { - editor.update(cx, |editor, cx| { - editor.toggle_warnings(&Default::default(), cx); - }); - } - })), - ) + h_flex() + .when(has_stale_excerpts, |div| { + div.child( + IconButton::new("update-excerpts", IconName::Update) + .icon_color(Color::Info) + .disabled(is_updating) + .tooltip(move |cx| Tooltip::text("Update excerpts", cx)) + .on_click(cx.listener(|this, _, cx| { + if let Some(editor) = + this.editor.as_ref().and_then(|editor| editor.upgrade()) + { + editor.update(cx, |editor, _| { + editor.enqueue_update_stale_excerpts(None); + }); + } + })), + ) + }) + .child( + IconButton::new("toggle-warnings", IconName::ExclamationTriangle) + .tooltip(move |cx| Tooltip::text(tooltip, cx)) + .on_click(cx.listener(|this, _, cx| { + if let Some(editor) = + this.editor.as_ref().and_then(|editor| editor.upgrade()) + { + editor.update(cx, |editor, cx| { + editor.toggle_warnings(&Default::default(), cx); + }); + } + })), + ) } } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6748a94b00..5cdc755808 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -714,6 +714,15 @@ impl FakeFs { Ok(()) } + pub fn read_file_sync(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + let path = normalize_path(path); + let state = self.state.lock(); + let entry = state.read_path(&path)?; + let entry = entry.lock(); + entry.file_content(&path).cloned() + } + async fn load_internal(&self, path: impl AsRef) -> Result> { let path = path.as_ref(); let path = normalize_path(path); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 34a6359db4..fa4c8d88d3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2699,7 +2699,6 @@ impl Project { for (_, _, server) in self.language_servers_for_worktree(worktree_id) { let text = include_text(server.as_ref()).then(|| buffer.read(cx).text()); - server .notify::( lsp::DidSaveTextDocumentParams { @@ -2710,46 +2709,8 @@ impl Project { .log_err(); } - let language_server_ids = self.language_server_ids_for_buffer(buffer.read(cx), cx); - for language_server_id in language_server_ids { - if let Some(LanguageServerState::Running { - adapter, - simulate_disk_based_diagnostics_completion, - .. - }) = self.language_servers.get_mut(&language_server_id) - { - // After saving a buffer using a language server that doesn't provide - // a disk-based progress token, kick off a timer that will reset every - // time the buffer is saved. If the timer eventually fires, simulate - // disk-based diagnostics being finished so that other pieces of UI - // (e.g., project diagnostics view, diagnostic status bar) can update. - // We don't emit an event right away because the language server might take - // some time to publish diagnostics. - if adapter.disk_based_diagnostics_progress_token.is_none() { - const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = - Duration::from_secs(1); - - let task = cx.spawn(move |this, mut cx| async move { - cx.background_executor().timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE).await; - if let Some(this) = this.upgrade() { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_finished( - language_server_id, - cx, - ); - this.enqueue_buffer_ordered_message( - BufferOrderedMessage::LanguageServerUpdate { - language_server_id, - message:proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(Default::default()) - }, - ) - .ok(); - }).ok(); - } - }); - *simulate_disk_based_diagnostics_completion = Some(task); - } - } + for language_server_id in self.language_server_ids_for_buffer(buffer.read(cx), cx) { + self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx); } } BufferEvent::FileHandleChanged => { @@ -2783,6 +2744,57 @@ impl Project { None } + // After saving a buffer using a language server that doesn't provide a disk-based progress token, + // kick off a timer that will reset every time the buffer is saved. If the timer eventually fires, + // simulate disk-based diagnostics being finished so that other pieces of UI (e.g., project + // diagnostics view, diagnostic status bar) can update. We don't emit an event right away because + // the language server might take some time to publish diagnostics. + fn simulate_disk_based_diagnostics_events_if_needed( + &mut self, + language_server_id: LanguageServerId, + cx: &mut ModelContext, + ) { + const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1); + + let Some(LanguageServerState::Running { + simulate_disk_based_diagnostics_completion, + adapter, + .. + }) = self.language_servers.get_mut(&language_server_id) + else { + return; + }; + + if adapter.disk_based_diagnostics_progress_token.is_some() { + return; + } + + let prev_task = simulate_disk_based_diagnostics_completion.replace(cx.spawn( + move |this, mut cx| async move { + cx.background_executor() + .timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE) + .await; + + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_finished(language_server_id, cx); + + if let Some(LanguageServerState::Running { + simulate_disk_based_diagnostics_completion, + .. + }) = this.language_servers.get_mut(&language_server_id) + { + *simulate_disk_based_diagnostics_completion = None; + } + }) + .ok(); + }, + )); + + if prev_task.is_none() { + self.disk_based_diagnostics_started(language_server_id, cx); + } + } + fn request_buffer_diff_recalculation( &mut self, buffer: &Model, @@ -4041,13 +4053,7 @@ impl Project { match progress { lsp::WorkDoneProgress::Begin(report) => { if is_disk_based_diagnostics_progress { - language_server_status.has_pending_diagnostic_updates = true; self.disk_based_diagnostics_started(language_server_id, cx); - self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate { - language_server_id, - message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(Default::default()) - }) - .ok(); } else { self.on_lsp_work_start( language_server_id, @@ -4092,18 +4098,7 @@ impl Project { language_server_status.progress_tokens.remove(&token); if is_disk_based_diagnostics_progress { - language_server_status.has_pending_diagnostic_updates = false; self.disk_based_diagnostics_finished(language_server_id, cx); - self.enqueue_buffer_ordered_message( - BufferOrderedMessage::LanguageServerUpdate { - language_server_id, - message: - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - Default::default(), - ), - }, - ) - .ok(); } else { self.on_lsp_work_end(language_server_id, token.clone(), cx); } @@ -7708,13 +7703,7 @@ impl Project { pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary { let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in - self.diagnostic_summaries(include_ignored, cx) - .filter(|(path, _, _)| { - let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored); - include_ignored || worktree == Some(false) - }) - { + for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) { summary.error_count += path_summary.error_count; summary.warning_count += path_summary.warning_count; } @@ -7726,20 +7715,23 @@ impl Project { include_ignored: bool, cx: &'a AppContext, ) -> impl Iterator + 'a { - self.visible_worktrees(cx) - .flat_map(move |worktree| { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - worktree - .diagnostic_summaries() - .map(move |(path, server_id, summary)| { - (ProjectPath { worktree_id, path }, server_id, summary) - }) - }) - .filter(move |(path, _, _)| { - let worktree = self.entry_for_path(path, cx).map(|entry| entry.is_ignored); - include_ignored || worktree == Some(false) - }) + self.visible_worktrees(cx).flat_map(move |worktree| { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + worktree + .diagnostic_summaries() + .filter_map(move |(path, server_id, summary)| { + if include_ignored + || worktree + .entry_for_path(path.as_ref()) + .map_or(false, |entry| !entry.is_ignored) + { + Some((ProjectPath { worktree_id, path }, server_id, summary)) + } else { + None + } + }) + }) } pub fn disk_based_diagnostics_started( @@ -7747,7 +7739,22 @@ impl Project { language_server_id: LanguageServerId, cx: &mut ModelContext, ) { + if let Some(language_server_status) = + self.language_server_statuses.get_mut(&language_server_id) + { + language_server_status.has_pending_diagnostic_updates = true; + } + cx.emit(Event::DiskBasedDiagnosticsStarted { language_server_id }); + if self.is_local() { + self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + Default::default(), + ), + }) + .ok(); + } } pub fn disk_based_diagnostics_finished( @@ -7755,7 +7762,23 @@ impl Project { language_server_id: LanguageServerId, cx: &mut ModelContext, ) { + if let Some(language_server_status) = + self.language_server_statuses.get_mut(&language_server_id) + { + language_server_status.has_pending_diagnostic_updates = false; + } + cx.emit(Event::DiskBasedDiagnosticsFinished { language_server_id }); + + if self.is_local() { + self.enqueue_buffer_ordered_message(BufferOrderedMessage::LanguageServerUpdate { + language_server_id, + message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + Default::default(), + ), + }) + .ok(); + } } pub fn active_entry(&self) -> Option { From f8beda0704543f0c01df743a65997d3565f99ecb Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 21:43:02 -0400 Subject: [PATCH 077/101] Rename `chat_with_functions` to use snake_case (#11020) This PR renames the `chat-with-functions.rs` example to use snake_case for the filename, as is convention. Release Notes: - N/A --- .../{chat-with-functions.rs => chat_with_functions.rs} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename crates/assistant2/examples/{chat-with-functions.rs => chat_with_functions.rs} (99%) diff --git a/crates/assistant2/examples/chat-with-functions.rs b/crates/assistant2/examples/chat_with_functions.rs similarity index 99% rename from crates/assistant2/examples/chat-with-functions.rs rename to crates/assistant2/examples/chat_with_functions.rs index 0a6ecbb02b..6c2870e680 100644 --- a/crates/assistant2/examples/chat-with-functions.rs +++ b/crates/assistant2/examples/chat_with_functions.rs @@ -1,4 +1,5 @@ -/// This example creates a basic Chat UI with a function for rolling a die. +//! This example creates a basic Chat UI with a function for rolling a die. + use anyhow::{Context as _, Result}; use assets::Assets; use assistant2::AssistantPanel; From d9eb3c4b35eb577b37c777c0f9dd874bb45eda77 Mon Sep 17 00:00:00 2001 From: DissolveDZ <68782699+DissolveDZ@users.noreply.github.com> Date: Fri, 26 Apr 2024 03:47:12 +0200 Subject: [PATCH 078/101] vim: Fix hollow cursor being offset when selecting text (#11000) Fixed the cursor selection being offset, the hollow cursor was being displayed fine when not having text selected that's why it might not have been noticed at first. Release Notes: - N/A Improved: https://github.com/zed-industries/zed/commit/0d6fb08b67e26f5e6abd14ff51b3a9ba1d89b9c0 --- crates/editor/src/element.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 14e5af444a..ada5e353e5 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -89,7 +89,10 @@ impl SelectionLayout { } // any vim visual mode (including line mode) - if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed { + if (cursor_shape == CursorShape::Block || cursor_shape == CursorShape::Hollow) + && !range.is_empty() + && !selection.reversed + { if head.column() > 0 { head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) } else if head.row() > 0 && head != map.max_point() { From 604857ed2e8a4b903e393dcfcfefb9d148b2ff8c Mon Sep 17 00:00:00 2001 From: Hendrik Sollich Date: Fri, 26 Apr 2024 03:47:52 +0200 Subject: [PATCH 079/101] vim: Increment search right (#10866) Hi there, nice editor! Here's my attempt at fixing #10865. Thanks Release Notes: -vim: Fix ctrl+a when cursor is on a decimal point ([#10865](https://github.com/zed-industries/zed/issues/10865)). --------- Co-authored-by: Conrad Irwin --- crates/vim/src/normal/increment.rs | 57 +++++++++++++++++-- .../test_data/test_increment_with_dot.json | 5 ++ .../test_increment_with_two_dots.json | 5 ++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 crates/vim/test_data/test_increment_with_dot.json create mode 100644 crates/vim/test_data/test_increment_with_two_dots.json diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index e70fce99e1..0ceb7c5a6d 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -117,13 +117,16 @@ fn find_number( ) -> Option<(Range, String, u32)> { let mut offset = start.to_offset(snapshot); - // go backwards to the start of any number the selection is within - for ch in snapshot.reversed_chars_at(offset) { - if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' { - offset -= ch.len_utf8(); - continue; + let ch0 = snapshot.chars_at(offset).next(); + if ch0.as_ref().is_some_and(char::is_ascii_digit) || matches!(ch0, Some('-' | 'b' | 'x')) { + // go backwards to the start of any number the selection is within + for ch in snapshot.reversed_chars_at(offset) { + if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' { + offset -= ch.len_utf8(); + continue; + } + break; } - break; } let mut begin = None; @@ -217,6 +220,48 @@ mod test { .await; } + #[gpui::test] + async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + 1ˇ.2 + "}) + .await; + + cx.simulate_shared_keystrokes(["ctrl-a"]).await; + cx.assert_shared_state(indoc! {" + 1.ˇ3 + "}) + .await; + cx.simulate_shared_keystrokes(["ctrl-x"]).await; + cx.assert_shared_state(indoc! {" + 1.ˇ2 + "}) + .await; + } + + #[gpui::test] + async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + 111.ˇ.2 + "}) + .await; + + cx.simulate_shared_keystrokes(["ctrl-a"]).await; + cx.assert_shared_state(indoc! {" + 111..ˇ3 + "}) + .await; + cx.simulate_shared_keystrokes(["ctrl-x"]).await; + cx.assert_shared_state(indoc! {" + 111..ˇ2 + "}) + .await; + } + #[gpui::test] async fn test_increment_radix(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_increment_with_dot.json b/crates/vim/test_data/test_increment_with_dot.json new file mode 100644 index 0000000000..b5c5b0914e --- /dev/null +++ b/crates/vim/test_data/test_increment_with_dot.json @@ -0,0 +1,5 @@ +{"Put":{"state":"1ˇ.2\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"1.ˇ3\n","mode":"Normal"}} +{"Key":"ctrl-x"} +{"Get":{"state":"1.ˇ2\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_two_dots.json b/crates/vim/test_data/test_increment_with_two_dots.json new file mode 100644 index 0000000000..38b38f1005 --- /dev/null +++ b/crates/vim/test_data/test_increment_with_two_dots.json @@ -0,0 +1,5 @@ +{"Put":{"state":"111.ˇ.2\n"}} +{"Key":"ctrl-a"} +{"Get":{"state":"111..ˇ3\n","mode":"Normal"}} +{"Key":"ctrl-x"} +{"Get":{"state":"111..ˇ2\n","mode":"Normal"}} From 1b614ef63be00be15aaef17d2437cbf955fca1cc Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 22:21:18 -0400 Subject: [PATCH 080/101] Add an Assistant example that can interact with the filesystem (#11027) This PR adds a new Assistant example that is able to interact with the filesystem using a tool. Release Notes: - N/A --- Cargo.lock | 1 + crates/assistant2/Cargo.toml | 5 +- .../assistant2/examples/file_interactions.rs | 221 ++++++++++++++++++ 3 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 crates/assistant2/examples/file_interactions.rs diff --git a/Cargo.lock b/Cargo.lock index 4c206e28c8..d36db0c3a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,7 @@ dependencies = [ "editor", "env_logger", "feature_flags", + "fs", "futures 0.3.28", "gpui", "language", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 886a84c863..4a0703f27b 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -19,15 +19,17 @@ assistant_tooling.workspace = true client.workspace = true editor.workspace = true feature_flags.workspace = true +fs.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true +nanoid = "0.4" open_ai.workspace = true project.workspace = true rich_text.workspace = true -semantic_index.workspace = true schemars.workspace = true +semantic_index.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true @@ -35,7 +37,6 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -nanoid = "0.4" [dev-dependencies] assets.workspace = true diff --git a/crates/assistant2/examples/file_interactions.rs b/crates/assistant2/examples/file_interactions.rs new file mode 100644 index 0000000000..c810085b86 --- /dev/null +++ b/crates/assistant2/examples/file_interactions.rs @@ -0,0 +1,221 @@ +//! This example creates a basic Chat UI for interacting with the filesystem. + +use anyhow::{Context as _, Result}; +use assets::Assets; +use assistant2::AssistantPanel; +use assistant_tooling::{LanguageModelTool, ToolRegistry}; +use client::Client; +use fs::Fs; +use futures::StreamExt; +use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions}; +use language::LanguageRegistry; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{KeymapFile, DEFAULT_KEYMAP_PATH}; +use std::path::PathBuf; +use std::sync::Arc; +use theme::LoadThemes; +use ui::{div, prelude::*, Render}; +use util::ResultExt as _; + +actions!(example, [Quit]); + +struct FileBrowserTool { + fs: Arc, + root_dir: PathBuf, +} + +impl FileBrowserTool { + fn new(fs: Arc, root_dir: PathBuf) -> Self { + Self { fs, root_dir } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +struct FileBrowserParams { + command: FileBrowserCommand, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +enum FileBrowserCommand { + Ls { path: PathBuf }, + Cat { path: PathBuf }, +} + +#[derive(Serialize, Deserialize)] +enum FileBrowserOutput { + Ls { entries: Vec }, + Cat { content: String }, +} + +pub struct FileBrowserView { + result: Result, +} + +impl Render for FileBrowserView { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let Ok(output) = self.result.as_ref() else { + return h_flex().child("Failed to perform operation"); + }; + + match output { + FileBrowserOutput::Ls { entries } => v_flex().children( + entries + .into_iter() + .map(|entry| h_flex().text_ui(cx).child(entry.clone())), + ), + FileBrowserOutput::Cat { content } => h_flex().child(content.clone()), + } + } +} + +impl LanguageModelTool for FileBrowserTool { + type Input = FileBrowserParams; + type Output = FileBrowserOutput; + type View = FileBrowserView; + + fn name(&self) -> String { + "file_browser".to_string() + } + + fn description(&self) -> String { + "A tool for browsing the filesystem.".to_string() + } + + fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task> { + cx.spawn({ + let fs = self.fs.clone(); + let root_dir = self.root_dir.clone(); + let input = input.clone(); + |_cx| async move { + match input.command { + FileBrowserCommand::Ls { path } => { + let path = root_dir.join(path); + + let mut output = fs.read_dir(&path).await?; + + let mut entries = Vec::new(); + while let Some(entry) = output.next().await { + let entry = entry?; + entries.push(entry.display().to_string()); + } + + Ok(FileBrowserOutput::Ls { entries }) + } + FileBrowserCommand::Cat { path } => { + let path = root_dir.join(path); + + let output = fs.load(&path).await?; + + Ok(FileBrowserOutput::Cat { content: output }) + } + } + } + }) + } + + fn new_view( + _tool_call_id: String, + _input: Self::Input, + result: Result, + cx: &mut WindowContext, + ) -> gpui::View { + cx.new_view(|_cx| FileBrowserView { result }) + } + + fn format(_input: &Self::Input, output: &Result) -> String { + let Ok(output) = output else { + return "Failed to perform command: {input:?}".to_string(); + }; + + match output { + FileBrowserOutput::Ls { entries } => entries.join("\n"), + FileBrowserOutput::Cat { content } => content.to_owned(), + } + } +} + +fn main() { + env_logger::init(); + App::new().with_assets(Assets).run(|cx| { + cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None))); + cx.on_action(|_: &Quit, cx: &mut AppContext| { + cx.quit(); + }); + + settings::init(cx); + language::init(cx); + Project::init_settings(cx); + editor::init(cx); + theme::init(LoadThemes::JustBase, cx); + Assets.load_fonts(cx).unwrap(); + KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap(); + client::init_settings(cx); + release_channel::init("0.130.0", cx); + + let client = Client::production(cx); + { + let client = client.clone(); + cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await }) + .detach_and_log_err(cx); + } + assistant2::init(client.clone(), cx); + + let language_registry = Arc::new(LanguageRegistry::new( + Task::ready(()), + cx.background_executor().clone(), + )); + let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client()); + languages::init(language_registry.clone(), node_runtime, cx); + + cx.spawn(|cx| async move { + cx.update(|cx| { + let fs = Arc::new(fs::RealFs::new(None)); + let cwd = std::env::current_dir().expect("Failed to get current working directory"); + + let mut tool_registry = ToolRegistry::new(); + tool_registry + .register(FileBrowserTool::new(fs, cwd)) + .context("failed to register FileBrowserTool") + .log_err(); + + let tool_registry = Arc::new(tool_registry); + + println!("Tools registered"); + for definition in tool_registry.definitions() { + println!("{}", definition); + } + + cx.open_window(WindowOptions::default(), |cx| { + cx.new_view(|cx| Example::new(language_registry, tool_registry, cx)) + }); + cx.activate(true); + }) + }) + .detach_and_log_err(cx); + }) +} + +struct Example { + assistant_panel: View, +} + +impl Example { + fn new( + language_registry: Arc, + tool_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + Self { + assistant_panel: cx + .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)), + } + } +} + +impl Render for Example { + fn render(&mut self, _cx: &mut ViewContext) -> impl ui::prelude::IntoElement { + div().size_full().child(self.assistant_panel.clone()) + } +} From 6d7332e80c4e313b6d217d6b3ce1baf694622016 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Apr 2024 20:32:15 -0600 Subject: [PATCH 081/101] Fix panic in vim search (#11022) Release Notes: - vim: Fixed a panic when searching --- crates/vim/src/normal/search.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 16ac5c0090..b2670ffdbc 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -159,11 +159,21 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte search_bar.select_match(direction, count, cx); search_bar.focus_editor(&Default::default(), cx); - let prior_selections = state.prior_selections.drain(..).collect(); + let mut prior_selections: Vec<_> = state.prior_selections.drain(..).collect(); let prior_mode = state.prior_mode; let prior_operator = state.prior_operator.take(); let new_selections = vim.editor_selections(cx); + // If the active editor has changed during a search, don't panic. + if prior_selections.iter().any(|s| { + vim.update_active_editor(cx, |_vim, editor, cx| { + !s.start.is_valid(&editor.snapshot(cx).buffer_snapshot) + }) + .unwrap_or(true) + }) { + prior_selections.clear(); + } + if prior_mode != vim.state().mode { vim.switch_mode(prior_mode, true, cx); } From a4ad3bcc08f69c97c8e5345ee3d4a89ebd97bf3b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 25 Apr 2024 22:37:40 -0400 Subject: [PATCH 082/101] Hoist `nanoid` to workspace-level (#11029) This PR hoists `nanoid` up to a workspace dependency. Release Notes: - N/A --- Cargo.toml | 1 + crates/assistant2/Cargo.toml | 2 +- crates/collab/Cargo.toml | 2 +- crates/live_kit_client/Cargo.toml | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c4200f3e2..0c093cca61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -283,6 +283,7 @@ itertools = "0.11.0" lazy_static = "1.4.0" linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } +nanoid = "0.4" ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 4a0703f27b..82b43dbaa4 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -24,7 +24,7 @@ futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true -nanoid = "0.4" +nanoid.workspace = true open_ai.workspace = true project.workspace = true rich_text.workspace = true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 1b58438e9a..25692ca690 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -37,7 +37,7 @@ google_ai.workspace = true hex.workspace = true live_kit_server.workspace = true log.workspace = true -nanoid = "0.4" +nanoid.workspace = true open_ai.workspace = true parking_lot.workspace = true prometheus = "0.13" diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 0d1cf08ad8..e893e0aab1 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -35,7 +35,7 @@ gpui = { workspace = true, optional = true } live_kit_server = { workspace = true, optional = true } log.workspace = true media.workspace = true -nanoid = { version = "0.4", optional = true} +nanoid = { workspace = true, optional = true} parking_lot.workspace = true postage.workspace = true @@ -47,14 +47,14 @@ async-trait = { workspace = true } collections = { workspace = true } gpui = { workspace = true } live_kit_server.workspace = true -nanoid = "0.4" +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 -nanoid = "0.4" +nanoid.workspace = true sha2.workspace = true simplelog = "0.9" From d9d509a2bb17d788b34fc614205e95c3bc1bbfd6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Apr 2024 21:07:06 -0600 Subject: [PATCH 083/101] Send installation id with crashes (#11032) This will let us prioritize crashes that affect many users. Release Notes: - N/A --- crates/collab/src/api/events.rs | 8 ++++++++ crates/zed/src/main.rs | 24 +++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 95abad1820..3c954d6014 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -136,6 +136,13 @@ pub async fn post_crash( .get("x-zed-panicked-on") .and_then(|h| h.to_str().ok()) .and_then(|s| s.parse().ok()); + + let installation_id = headers + .get("x-zed-installation-id") + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_default(); + let mut recent_panic = None; if let Some(recent_panic_on) = recent_panic_on { @@ -160,6 +167,7 @@ pub async fn post_crash( os_version = %report.header.os_version, bundle_id = %report.header.bundle_id, incident_id = %report.header.incident_id, + installation_id = %installation_id, description = %description, backtrace = %summary, "crash report"); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0ee87f7f41..97b0526bbc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -145,6 +145,13 @@ fn init_headless(dev_server_token: DevServerToken) { ); handle_settings_file_changes(user_settings_file_rx, cx); + let (installation_id, _) = cx + .background_executor() + .block(installation_id()) + .ok() + .unzip(); + upload_panics_and_crashes(client.http_client(), installation_id, cx); + headless::init( client.clone(), headless::AppState { @@ -323,7 +330,7 @@ fn init_ui(args: Args) { .detach(); let telemetry = client.telemetry(); - telemetry.start(installation_id, session_id, cx); + telemetry.start(installation_id.clone(), session_id, cx); telemetry.report_setting_event("theme", cx.theme().name.to_string()); telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string()); telemetry.report_app_event( @@ -378,8 +385,7 @@ fn init_ui(args: Args) { cx.set_menus(app_menus()); initialize_workspace(app_state.clone(), cx); - // todo(linux): unblock this - upload_panics_and_crashes(client.http_client(), cx); + upload_panics_and_crashes(client.http_client(), installation_id, cx); cx.activate(true); @@ -824,7 +830,11 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin })); } -fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) { +fn upload_panics_and_crashes( + http: Arc, + installation_id: Option, + cx: &mut AppContext, +) { let telemetry_settings = *client::TelemetrySettings::get_global(cx); cx.background_executor() .spawn(async move { @@ -832,7 +842,7 @@ fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) .await .log_err() .flatten(); - upload_previous_crashes(http, most_recent_panic, telemetry_settings) + upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings) .await .log_err() }) @@ -915,6 +925,7 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED"; async fn upload_previous_crashes( http: Arc, most_recent_panic: Option<(i64, String)>, + installation_id: Option, telemetry_settings: client::TelemetrySettings, ) -> Result<()> { if !telemetry_settings.diagnostics { @@ -964,6 +975,9 @@ async fn upload_previous_crashes( .header("x-zed-panicked-on", format!("{}", panicked_on)) .header("x-zed-panic", payload) } + if let Some(installation_id) = installation_id.as_ref() { + request = request.header("x-zed-installation-id", installation_id); + } let request = request.body(body.into())?; From 5c2f27a50158c64fadac890a0f7ed6a7d6d8e6f6 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 26 Apr 2024 11:09:06 +0800 Subject: [PATCH 084/101] Fix VIM cw on last character of a word doesn't work as expected: (#10963) At the moment, using the default expand_selection seems to do the job well, without the need for some additional logic, which may also make the code a little clearer, Fix #10945 Release Notes: - N/A --- crates/vim/src/normal/change.rs | 120 ++++++++++++------------ crates/vim/test_data/test_change_w.json | 4 + 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 97608d4123..4fca6b2ccd 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -31,48 +31,42 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m editor.set_clip_at_line_ends(false, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { - motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion - { - expand_changed_word_selection( - map, - selection, - times, - ignore_punctuation, - &text_layout_details, - false, - ) - } else if let Motion::NextSubwordStart { ignore_punctuation } = motion { - expand_changed_word_selection( - map, - selection, - times, - ignore_punctuation, - &text_layout_details, - true, - ) - } else { - let result = motion.expand_selection( - map, - selection, - times, - false, - &text_layout_details, - ); - if let Motion::CurrentLine = motion { - let mut start_offset = selection.start.to_offset(map, Bias::Left); - let scope = map - .buffer_snapshot - .language_scope_at(selection.start.to_point(&map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace { - break; - } - start_offset = offset + ch.len_utf8(); - } - selection.start = start_offset.to_display_point(map); + motion_succeeded |= match motion { + Motion::NextWordStart { ignore_punctuation } + | Motion::NextSubwordStart { ignore_punctuation } => { + expand_changed_word_selection( + map, + selection, + times, + ignore_punctuation, + &text_layout_details, + motion == Motion::NextSubwordStart { ignore_punctuation }, + ) } - result - }; + _ => { + let result = motion.expand_selection( + map, + selection, + times, + false, + &text_layout_details, + ); + if let Motion::CurrentLine = motion { + let mut start_offset = selection.start.to_offset(map, Bias::Left); + let scope = map + .buffer_snapshot + .language_scope_at(selection.start.to_point(&map)); + for (ch, offset) in map.buffer_chars_at(start_offset) { + if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace { + break; + } + start_offset = offset + ch.len_utf8(); + } + selection.start = start_offset.to_display_point(map); + } + result + } + } }); }); copy_selections_content(vim, editor, motion.linewise(), cx); @@ -116,8 +110,8 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo // Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is // on a non-blank. This is because "cw" is interpreted as change-word, and a // word does not include the following white space. {Vi: "cw" when on a blank -// followed by other blanks changes only the first blank; this is probably a -// bug, because "dw" deletes all the blanks} +// followed by other blanks changes only the first blank; this is probably a +// bug, because "dw" deletes all the blanks} fn expand_changed_word_selection( map: &DisplaySnapshot, selection: &mut Selection, @@ -126,7 +120,7 @@ fn expand_changed_word_selection( text_layout_details: &TextLayoutDetails, use_subword: bool, ) -> bool { - if times.is_none() || times.unwrap() == 1 { + let is_in_word = || { let scope = map .buffer_snapshot .language_scope_at(selection.start.to_point(map)); @@ -135,25 +129,28 @@ fn expand_changed_word_selection( .next() .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or_default(); - - if in_word { - if use_subword { - selection.end = - motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false); - } else { - selection.end = - motion::next_word_end(map, selection.end, ignore_punctuation, 1, false); + return in_word; + }; + if (times.is_none() || times.unwrap() == 1) && is_in_word() { + let next_char = map + .buffer_chars_at( + motion::next_char(map, selection.end, false).to_offset(map, Bias::Left), + ) + .next(); + match next_char { + Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false), + _ => { + if use_subword { + selection.end = + motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false); + } else { + selection.end = + motion::next_word_end(map, selection.end, ignore_punctuation, 1, false); + } + selection.end = motion::next_char(map, selection.end, false); } - selection.end = motion::next_char(map, selection.end, false); - true - } else { - let motion = if use_subword { - Motion::NextSubwordStart { ignore_punctuation } - } else { - Motion::NextWordStart { ignore_punctuation } - }; - motion.expand_selection(map, selection, None, false, &text_layout_details) } + true } else { let motion = if use_subword { Motion::NextSubwordStart { ignore_punctuation } @@ -209,6 +206,7 @@ mod test { cx.assert("Teˇst").await; cx.assert("Tˇest test").await; cx.assert("Testˇ test").await; + cx.assert("Tesˇt test").await; cx.assert(indoc! {" Test teˇst test"}) diff --git a/crates/vim/test_data/test_change_w.json b/crates/vim/test_data/test_change_w.json index 586fbdf799..27be543532 100644 --- a/crates/vim/test_data/test_change_w.json +++ b/crates/vim/test_data/test_change_w.json @@ -10,6 +10,10 @@ {"Key":"c"} {"Key":"w"} {"Get":{"state":"Testˇtest","mode":"Insert"}} +{"Put":{"state":"Tesˇt test"}} +{"Key":"c"} +{"Key":"w"} +{"Get":{"state":"Tesˇ test","mode":"Insert"}} {"Put":{"state":"Test teˇst\ntest"}} {"Key":"c"} {"Key":"w"} From 03d0b68f0cd45df0a63fecd565a1db3482f9e0ef Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Apr 2024 21:29:56 -0600 Subject: [PATCH 085/101] Fix panic in rename selections (#11033) cc @someonetoignore Release Notes: - Fixed a panic when renaming with a selection (preview only) --- crates/editor/src/editor.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f51c3237ac..b2b299e780 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8206,9 +8206,13 @@ impl Editor { cursor_offset_in_rename_range_end..cursor_offset_in_rename_range } }; - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([rename_selection_range]); - }); + if rename_selection_range.end > old_name.len() { + editor.select_all(&SelectAll, cx); + } else { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([rename_selection_range]); + }); + } editor }); From 7f229dc202d8e98ce05406c9dc8028a26bd382c1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 25 Apr 2024 21:50:31 -0600 Subject: [PATCH 086/101] Remove unread notes indicator for now (#11035) I'd like to add something back here, but it's more distracting than helpful today. Fixes: #10887 Release Notes: - Removed channel notes unread indicator --------- Co-authored-by: Marshall Bowers --- crates/collab_ui/src/collab_panel.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d9b3f1abbf..7f843b7a86 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2970,6 +2970,7 @@ impl Render for DraggedChannelView { struct JoinChannelTooltip { channel_store: Model, channel_id: ChannelId, + #[allow(unused)] has_notes_notification: bool, } @@ -2983,12 +2984,6 @@ impl Render for JoinChannelTooltip { container .child(Label::new("Join channel")) - .children(self.has_notes_notification.then(|| { - h_flex() - .gap_2() - .child(Indicator::dot().color(Color::Info)) - .child(Label::new("Unread notes")) - })) .children(participants.iter().map(|participant| { h_flex() .gap_2() From eb7bd0b98a138074f9f6c48a5b5aafc5195d1e89 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 26 Apr 2024 01:42:21 -0600 Subject: [PATCH 087/101] Use fewer fancy cursors even for vim users (#11041) Release Notes: - N/A --- crates/vim/src/editor_events.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 9ddcea3852..b1aa44f848 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -55,7 +55,9 @@ fn blurred(editor: View, cx: &mut WindowContext) { } } editor.update(cx, |editor, cx| { - editor.set_cursor_shape(language::CursorShape::Hollow, cx); + if editor.use_modal_editing() { + editor.set_cursor_shape(language::CursorShape::Hollow, cx); + } }); }); } From bacc92333aedf80870b30e66dc8902113e6f5fe2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:29:16 +0200 Subject: [PATCH 088/101] tasks: Fix divider position in modal (#11049) The divider between templates and recent runs is constant, regardless of the currently used filter string; this can lead to situations where an user can remove the predefined task, which isn't good at all. Additionally, in this PR I've made it so that recent runs always show up before task templates in filtered list. Release Notes: - Fixed position of list divider in task modal. --- crates/tasks_ui/src/modal.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 156eab497f..9e07e57dad 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -62,6 +62,7 @@ pub(crate) struct TasksModalDelegate { inventory: Model, candidates: Option>, last_used_candidate_index: Option, + divider_index: Option, matches: Vec, selected_index: usize, workspace: WeakView, @@ -82,6 +83,7 @@ impl TasksModalDelegate { candidates: None, matches: Vec::new(), last_used_candidate_index: None, + divider_index: None, selected_index: 0, prompt: String::default(), task_context, @@ -255,7 +257,17 @@ impl PickerDelegate for TasksModalDelegate { .update(&mut cx, |picker, _| { let delegate = &mut picker.delegate; delegate.matches = matches; + if let Some(index) = delegate.last_used_candidate_index { + delegate.matches.sort_by_key(|m| m.candidate_id > index); + } + delegate.prompt = query; + delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| { + let index = delegate + .matches + .partition_point(|matching_task| matching_task.candidate_id <= index); + Some(index).and_then(|index| (index != 0).then(|| index - 1)) + }); if delegate.matches.is_empty() { delegate.selected_index = 0; @@ -352,7 +364,7 @@ impl PickerDelegate for TasksModalDelegate { }) .map(|item| { let item = if matches!(source_kind, TaskSourceKind::UserInput) - || Some(ix) <= self.last_used_candidate_index + || Some(ix) <= self.divider_index { let task_index = hit.candidate_id; let delete_button = div().child( @@ -412,7 +424,7 @@ impl PickerDelegate for TasksModalDelegate { } fn separators_after_indices(&self) -> Vec { - if let Some(i) = self.last_used_candidate_index { + if let Some(i) = self.divider_index { vec![i] } else { Vec::new() From 8006f6951339992187c277c18de9f0a32456524a Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 26 Apr 2024 14:31:04 +0200 Subject: [PATCH 089/101] Fix `Cargo.toml` typo ref -> rev (#11047) Release Notes: - N/A --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d36db0c3a3..0d4c9518ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10625,7 +10625,7 @@ dependencies = [ [[package]] name = "tree-sitter-jsdoc" version = "0.20.0" -source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" +source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc?rev=6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index 0c093cca61..579ef9799d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,7 +342,7 @@ tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" } rustc-demangle = "0.1.23" tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-html = "0.19.0" -tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" } +tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" } From 1af1a9e8b330bced049d82aad39500353062974f Mon Sep 17 00:00:00 2001 From: Tim Masliuchenko Date: Fri, 26 Apr 2024 16:56:34 +0100 Subject: [PATCH 090/101] Toggle tasks modal in task::Rerun, when no tasks have been scheduled (#11059) Currently, when no tasks have been scheduled, the `task::Rerun` action does nothing. This PR adds a fallback, so when no tasks have been scheduled so far the `task::Rerun` action toggles the tasks modal https://github.com/zed-industries/zed/assets/471335/72f7a71e-cfa8-49db-a295-fb05b2e7c905 Release Notes: - Improved the `task::Rerun` action to toggle the tasks modal when no tasks have been scheduled so far --- crates/tasks_ui/src/lib.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 4898c8af0d..2b11fba49a 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -66,6 +66,8 @@ pub fn init(cx: &mut AppContext) { cx, ); } + } else { + toggle_modal(workspace, cx); }; }); }, @@ -76,17 +78,19 @@ pub fn init(cx: &mut AppContext) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext) { match &action.task_name { Some(name) => spawn_task_with_name(name.clone(), cx), - None => { - let inventory = workspace.project().read(cx).task_inventory().clone(); - let workspace_handle = workspace.weak_handle(); - let task_context = task_context(workspace, cx); - workspace.toggle_modal(cx, |cx| { - TasksModal::new(inventory, task_context, workspace_handle, cx) - }) - } + None => toggle_modal(workspace, cx), } } +fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) { + let inventory = workspace.project().read(cx).task_inventory().clone(); + let workspace_handle = workspace.weak_handle(); + let task_context = task_context(workspace, cx); + workspace.toggle_modal(cx, |cx| { + TasksModal::new(inventory, task_context, workspace_handle, cx) + }) +} + fn spawn_task_with_name(name: String, cx: &mut ViewContext) { cx.spawn(|workspace, mut cx| async move { let did_spawn = workspace From 314b723292f1195d695e923eb8ffff16f1169aa0 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:04:34 +0200 Subject: [PATCH 091/101] remote projects: Allow reusing window (#11058) Release Notes: - Allow reusing the window when opening a remote project from the recent projects picker - Fixed an issue, which would not let you rejoin a remote project after disconnecting from it for the first time --------- Co-authored-by: Conrad Co-authored-by: Remco --- crates/collab/src/tests/dev_server_tests.rs | 9 ++++- crates/recent_projects/src/recent_projects.rs | 39 +++++++++++++++---- crates/recent_projects/src/remote_projects.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 27 ++++++++++--- crates/workspace/src/workspace.rs | 26 +++++++++---- 5 files changed, 80 insertions(+), 23 deletions(-) diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 40ecc66bd7..0917f4994f 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -70,6 +70,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC workspace::join_remote_project( projects[0].project_id.unwrap(), client.app_state.clone(), + None, cx, ) }) @@ -205,7 +206,12 @@ async fn create_remote_project( let projects = store.remote_projects(); assert_eq!(projects.len(), 1); assert_eq!(projects[0].path, "/remote"); - workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx) + workspace::join_remote_project( + projects[0].project_id.unwrap(), + client_app_state, + None, + cx, + ) }) .await .unwrap(); @@ -301,6 +307,7 @@ async fn test_dev_server_reconnect( workspace::join_remote_project( projects[0].project_id.unwrap(), client2.app_state.clone(), + None, cx, ) }) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 6090590b17..813f95e212 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -310,7 +310,6 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, cx) } } - //TODO support opening remote projects in the same window SerializedWorkspaceLocation::Remote(remote_project) => { let store = ::remote_projects::Store::global(cx).read(cx); let Some(project_id) = store @@ -338,12 +337,38 @@ impl PickerDelegate for RecentProjectsDelegate { }) }; if let Some(app_state) = AppState::global(cx).upgrade() { - let task = - workspace::join_remote_project(project_id, app_state, cx); - cx.spawn(|_, _| async move { - task.await?; - Ok(()) - }) + let handle = if replace_current_window { + cx.window_handle().downcast::() + } else { + None + }; + + if let Some(handle) = handle { + cx.spawn(move |workspace, mut cx| async move { + let continue_replacing = workspace + .update(&mut cx, |workspace, cx| { + workspace. + prepare_to_close(true, cx) + })? + .await?; + if continue_replacing { + workspace + .update(&mut cx, |_workspace, cx| { + workspace::join_remote_project(project_id, app_state, Some(handle), cx) + })? + .await?; + } + Ok(()) + }) + } + else { + let task = + workspace::join_remote_project(project_id, app_state, None, cx); + cx.spawn(|_, _| async move { + task.await?; + Ok(()) + }) + } } else { Task::ready(Err(anyhow::anyhow!("App state not found"))) } diff --git a/crates/recent_projects/src/remote_projects.rs b/crates/recent_projects/src/remote_projects.rs index 8a548c747e..447b22771c 100644 --- a/crates/recent_projects/src/remote_projects.rs +++ b/crates/recent_projects/src/remote_projects.rs @@ -386,7 +386,7 @@ impl RemoteProjects { .on_click(cx.listener(move |_, _, cx| { if let Some(project_id) = project_id { if let Some(app_state) = AppState::global(cx).upgrade() { - workspace::join_remote_project(project_id, app_state, cx) + workspace::join_remote_project(project_id, app_state, None, cx) .detach_and_prompt_err("Could not join project", cx, |_, _| None) } } else { diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index c3eccd95f6..097a050ee8 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -9,8 +9,8 @@ use fs::Fs; use futures::stream::StreamExt; use futures_batch::ChunksTimeoutStreamExt; use gpui::{ - AppContext, AsyncAppContext, Context, EntityId, EventEmitter, Global, Model, ModelContext, - Subscription, Task, WeakModel, + AppContext, AsyncAppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, Global, + Model, ModelContext, Subscription, Task, WeakModel, }; use heed::types::{SerdeBincode, Str}; use language::LanguageRegistry; @@ -68,6 +68,18 @@ impl SemanticIndex { project: Model, cx: &mut AppContext, ) -> Model { + let project_weak = project.downgrade(); + project.update(cx, move |_, cx| { + cx.on_release(move |_, cx| { + if cx.has_global::() { + cx.update_global::(|this, _| { + this.project_indices.remove(&project_weak); + }) + } + }) + .detach(); + }); + self.project_indices .entry(project.downgrade()) .or_insert_with(|| { @@ -86,7 +98,7 @@ impl SemanticIndex { pub struct ProjectIndex { db_connection: heed::Env, - project: Model, + project: WeakModel, worktree_indices: HashMap, language_registry: Arc, fs: Arc, @@ -116,7 +128,7 @@ impl ProjectIndex { let fs = project.read(cx).fs().clone(); let mut this = ProjectIndex { db_connection, - project: project.clone(), + project: project.downgrade(), worktree_indices: HashMap::default(), language_registry, fs, @@ -143,8 +155,11 @@ impl ProjectIndex { } fn update_worktree_indices(&mut self, cx: &mut ModelContext) { - let worktrees = self - .project + let Some(project) = self.project.upgrade() else { + return; + }; + + let worktrees = project .read(cx) .visible_worktrees(cx) .filter_map(|worktree| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8e29ce22e0..98cddd8d25 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4785,6 +4785,7 @@ pub fn join_hosted_project( pub fn join_remote_project( project_id: ProjectId, app_state: Arc, + window_to_replace: Option>, cx: &mut AppContext, ) -> Task>> { let windows = cx.windows(); @@ -4816,16 +4817,25 @@ pub fn join_remote_project( ) .await?; - let window_bounds_override = window_bounds_env_override(); - cx.update(|cx| { - let mut options = (app_state.build_window_options)(None, cx); - options.bounds = window_bounds_override; - cx.open_window(options, |cx| { - cx.new_view(|cx| { + if let Some(window_to_replace) = window_to_replace { + cx.update_window(window_to_replace.into(), |_, cx| { + cx.replace_root_view(|cx| { Workspace::new(Default::default(), project, app_state.clone(), cx) + }); + })?; + window_to_replace + } else { + let window_bounds_override = window_bounds_env_override(); + cx.update(|cx| { + let mut options = (app_state.build_window_options)(None, cx); + options.bounds = window_bounds_override; + cx.open_window(options, |cx| { + cx.new_view(|cx| { + Workspace::new(Default::default(), project, app_state.clone(), cx) + }) }) - }) - })? + })? + } }; workspace.update(&mut cx, |_, cx| { From 664f779eb47341960468fcfc16d40751d7d1a953 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 26 Apr 2024 13:25:25 -0600 Subject: [PATCH 092/101] new path picker (#11015) Still TODO: * Disable the new save-as for local projects * Wire up sending the new path to the remote server Release Notes: - Added the ability to "Save-as" in remote projects --------- Co-authored-by: Nathan Co-authored-by: Bennet --- Cargo.lock | 1 + README.md | 51 -- crates/collab/src/tests/dev_server_tests.rs | 32 ++ crates/collab/src/tests/integration_tests.rs | 7 +- crates/diagnostics/src/diagnostics.rs | 3 +- crates/diagnostics/src/diagnostics_tests.rs | 5 +- crates/editor/src/items.rs | 11 +- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 5 + crates/file_finder/src/new_path_prompt.rs | 463 ++++++++++++++++++ crates/gpui/src/platform.rs | 6 +- crates/gpui/src/platform/mac/window.rs | 9 +- crates/gpui/src/platform/windows/window.rs | 2 +- crates/gpui/src/style.rs | 7 + crates/picker/src/picker.rs | 30 +- crates/project/src/project.rs | 70 ++- crates/project/src/project_tests.rs | 7 +- crates/project_panel/src/project_panel.rs | 2 +- crates/recent_projects/src/remote_projects.rs | 2 +- crates/rpc/proto/zed.proto | 6 + crates/search/src/project_search.rs | 6 +- .../src/components/label/highlighted_label.rs | 65 ++- crates/workspace/src/item.rs | 11 +- crates/workspace/src/notifications.rs | 2 +- crates/workspace/src/pane.rs | 14 +- crates/workspace/src/workspace.rs | 59 +++ crates/worktree/src/worktree.rs | 47 +- 27 files changed, 775 insertions(+), 149 deletions(-) create mode 100644 crates/file_finder/src/new_path_prompt.rs diff --git a/Cargo.lock b/Cargo.lock index 0d4c9518ba..6302e78434 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3815,6 +3815,7 @@ dependencies = [ "ctor", "editor", "env_logger", + "futures 0.3.28", "fuzzy", "gpui", "itertools 0.11.0", diff --git a/README.md b/README.md index 186aa4ab70..e69de29bb2 100644 --- a/README.md +++ b/README.md @@ -1,51 +0,0 @@ -# Zed - -[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) - -Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). - -## Installation - -You can [download](https://zed.dev/download) Zed today for macOS (v10.15+). - -Support for additional platforms is on our [roadmap](https://zed.dev/roadmap): - -- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015)) -- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394)) -- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396)) - -For macOS users, you can also install Zed using [Homebrew](https://brew.sh/): - -```sh -brew install --cask zed -``` - -Alternatively, to install the Preview release: - -```sh -brew tap homebrew/cask-versions -brew install zed-preview -``` - -## Developing Zed - -- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md) -- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md) -- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md) -- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md) - -## Contributing - -See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed. - -Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles. - -## Licensing - -License information for third party dependencies must be correctly provided for CI to pass. - -We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following: - -- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml. -- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`. -- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration). diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 0917f4994f..8769b721b3 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -366,3 +366,35 @@ async fn test_create_remote_project_path_validation( ErrorCode::RemoteProjectPathDoesNotExist )); } + +#[gpui::test] +async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) { + let (server, client1) = TestServer::start1(cx1).await; + + // Creating a project with a path that does exist should not fail + let (dev_server, remote_workspace) = + create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await; + + let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1); + + cx.simulate_keystrokes("cmd-p 1 enter"); + cx.simulate_keystrokes("cmd-shift-s"); + cx.simulate_input("2.txt"); + cx.simulate_keystrokes("enter"); + + cx.executor().run_until_parked(); + + let title = remote_workspace + .update(&mut cx, |ws, cx| { + ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap() + }) + .unwrap(); + + assert_eq!(title, "2.txt"); + + let path = Path::new("/remote/2.txt"); + assert_eq!( + dev_server.fs().load(&path).await.unwrap(), + "remote\nremote\nremote" + ); +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 65e57a8ff3..e4fb75514f 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2468,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes( }); project_a .update(cx_a, |project, cx| { - project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx) + let path = ProjectPath { + path: Arc::from(Path::new("file3.rs")), + worktree_id: worktree_a.read(cx).id(), + }; + + project.save_buffer_as(new_buffer_a.clone(), path, cx) }) .await .unwrap(); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b59e819db2..d06ff824fb 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -36,7 +36,6 @@ use std::{ cmp::Ordering, mem, ops::Range, - path::PathBuf, }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; @@ -740,7 +739,7 @@ impl Item for ProjectDiagnosticsEditor { fn save_as( &mut self, _: Model, - _: PathBuf, + _: ProjectPath, _: &mut ViewContext, ) -> Task> { unreachable!() diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 3e7b4b67f2..a1bbd26a2b 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -13,7 +13,10 @@ use project::FakeFs; use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng}; use serde_json::json; use settings::SettingsStore; -use std::{env, path::Path}; +use std::{ + env, + path::{Path, PathBuf}, +}; use unindent::Unindent as _; use util::{post_inc, RandomCharIter}; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index da617c3fea..aa6b36d597 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -26,7 +26,7 @@ use std::{ cmp::{self, Ordering}, iter, ops::Range, - path::{Path, PathBuf}, + path::Path, sync::Arc, }; use text::{BufferId, Selection}; @@ -750,7 +750,7 @@ impl Item for Editor { fn save_as( &mut self, project: Model, - abs_path: PathBuf, + path: ProjectPath, cx: &mut ViewContext, ) -> Task> { let buffer = self @@ -759,14 +759,13 @@ impl Item for Editor { .as_singleton() .expect("cannot call save_as on an excerpt list"); - let file_extension = abs_path + let file_extension = path + .path .extension() .map(|a| a.to_string_lossy().to_string()); self.report_editor_event("save", file_extension, cx); - project.update(cx, |project, cx| { - project.save_buffer_as(buffer, abs_path, cx) - }) + project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx)) } fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 03411c130a..2fe030bf7b 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -16,6 +16,7 @@ doctest = false anyhow.workspace = true collections.workspace = true editor.workspace = true +futures.workspace = true fuzzy.workspace = true gpui.workspace = true itertools = "0.11" diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 8447dc8a55..096e8c0eaa 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod file_finder_tests; +mod new_path_prompt; + use collections::{HashMap, HashSet}; use editor::{scroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; @@ -10,6 +12,7 @@ use gpui::{ ViewContext, VisualContext, WeakView, }; use itertools::Itertools; +use new_path_prompt::NewPathPrompt; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use settings::Settings; @@ -37,6 +40,7 @@ pub struct FileFinder { pub fn init(cx: &mut AppContext) { cx.observe_new_views(FileFinder::register).detach(); + cx.observe_new_views(NewPathPrompt::register).detach(); } impl FileFinder { @@ -454,6 +458,7 @@ impl FileFinderDelegate { .root_entry() .map_or(false, |entry| entry.is_ignored), include_root_name, + directories_only: false, } }) .collect::>(); diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs new file mode 100644 index 0000000000..e538576b98 --- /dev/null +++ b/crates/file_finder/src/new_path_prompt.rs @@ -0,0 +1,463 @@ +use futures::channel::oneshot; +use fuzzy::PathMatch; +use gpui::{HighlightStyle, Model, StyledText}; +use picker::{Picker, PickerDelegate}; +use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use std::{ + path::PathBuf, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; +use ui::{highlight_ranges, prelude::*, LabelLike, ListItemSpacing}; +use ui::{ListItem, ViewContext}; +use util::ResultExt; +use workspace::Workspace; + +pub(crate) struct NewPathPrompt; + +#[derive(Debug, Clone)] +struct Match { + path_match: Option, + suffix: Option, +} + +impl Match { + fn entry<'a>(&'a self, project: &'a Project, cx: &'a WindowContext) -> Option<&'a Entry> { + if let Some(suffix) = &self.suffix { + let (worktree, path) = if let Some(path_match) = &self.path_match { + ( + project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx), + path_match.path.join(suffix), + ) + } else { + (project.worktrees().next(), PathBuf::from(suffix)) + }; + + worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path)) + } else if let Some(path_match) = &self.path_match { + let worktree = + project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?; + worktree.read(cx).entry_for_path(path_match.path.as_ref()) + } else { + None + } + } + + fn is_dir(&self, project: &Project, cx: &WindowContext) -> bool { + self.entry(project, cx).is_some_and(|e| e.is_dir()) + || self.suffix.as_ref().is_some_and(|s| s.ends_with('/')) + } + + fn relative_path(&self) -> String { + if let Some(path_match) = &self.path_match { + if let Some(suffix) = &self.suffix { + format!( + "{}/{}", + path_match.path.to_string_lossy(), + suffix.trim_end_matches('/') + ) + } else { + path_match.path.to_string_lossy().to_string() + } + } else if let Some(suffix) = &self.suffix { + suffix.trim_end_matches('/').to_string() + } else { + "".to_string() + } + } + + 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 { + project.worktrees().next()?.read(cx).id() + }; + + let path = PathBuf::from(self.relative_path()); + + Some(ProjectPath { + worktree_id, + path: Arc::from(path), + }) + } + + fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option { + let worktree = project.worktrees().next()?.read(cx); + let mut prefix = PathBuf::new(); + let parts = self.suffix.as_ref()?.split('/'); + for part in parts { + if worktree.entry_for_path(prefix.join(&part)).is_none() { + return Some(prefix); + } + prefix = prefix.join(part); + } + + None + } + + fn styled_text(&self, project: &Project, cx: &WindowContext) -> StyledText { + let mut text = "./".to_string(); + let mut highlights = Vec::new(); + let mut offset = text.as_bytes().len(); + + let separator = '/'; + let dir_indicator = "[…]"; + + if let Some(path_match) = &self.path_match { + text.push_str(&path_match.path.to_string_lossy()); + for (range, style) in highlight_ranges( + &path_match.path.to_string_lossy(), + &path_match.positions, + gpui::HighlightStyle::color(Color::Accent.color(cx)), + ) { + highlights.push((range.start + offset..range.end + offset, style)) + } + text.push(separator); + offset = text.as_bytes().len(); + + if let Some(suffix) = &self.suffix { + text.push_str(suffix); + let entry = self.entry(project, cx); + let color = if let Some(entry) = entry { + if entry.is_dir() { + Color::Accent + } else { + Color::Conflict + } + } else { + Color::Created + }; + highlights.push(( + offset..offset + suffix.as_bytes().len(), + HighlightStyle::color(color.color(cx)), + )); + offset += suffix.as_bytes().len(); + if entry.is_some_and(|e| e.is_dir()) { + text.push(separator); + offset += separator.len_utf8(); + + text.push_str(dir_indicator); + highlights.push(( + offset..offset + dir_indicator.bytes().len(), + HighlightStyle::color(Color::Muted.color(cx)), + )); + } + } else { + text.push_str(dir_indicator); + highlights.push(( + offset..offset + dir_indicator.bytes().len(), + HighlightStyle::color(Color::Muted.color(cx)), + )) + } + } else if let Some(suffix) = &self.suffix { + text.push_str(suffix); + let existing_prefix_len = self + .existing_prefix(project, cx) + .map(|prefix| prefix.to_string_lossy().as_bytes().len()) + .unwrap_or(0); + + if existing_prefix_len > 0 { + highlights.push(( + offset..offset + existing_prefix_len, + HighlightStyle::color(Color::Accent.color(cx)), + )); + } + highlights.push(( + offset + existing_prefix_len..offset + suffix.as_bytes().len(), + HighlightStyle::color(if self.entry(project, cx).is_some() { + Color::Conflict.color(cx) + } else { + Color::Created.color(cx) + }), + )); + offset += suffix.as_bytes().len(); + if suffix.ends_with('/') { + text.push_str(dir_indicator); + highlights.push(( + offset..offset + dir_indicator.bytes().len(), + HighlightStyle::color(Color::Muted.color(cx)), + )); + } + } + + StyledText::new(text).with_highlights(&cx.text_style().clone(), highlights) + } +} + +pub struct NewPathDelegate { + project: Model, + tx: Option>>, + selected_index: usize, + matches: Vec, + last_selected_dir: Option, + cancel_flag: Arc, + should_dismiss: bool, +} + +impl NewPathPrompt { + pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + if workspace.project().read(cx).is_remote() { + workspace.set_prompt_for_new_path(Box::new(|workspace, cx| { + let (tx, rx) = futures::channel::oneshot::channel(); + Self::prompt_for_new_path(workspace, tx, cx); + rx + })); + } + } + + fn prompt_for_new_path( + workspace: &mut Workspace, + tx: oneshot::Sender>, + cx: &mut ViewContext, + ) { + let project = workspace.project().clone(); + workspace.toggle_modal(cx, |cx| { + let delegate = NewPathDelegate { + project, + tx: Some(tx), + selected_index: 0, + matches: vec![], + cancel_flag: Arc::new(AtomicBool::new(false)), + last_selected_dir: None, + should_dismiss: true, + }; + + Picker::uniform_list(delegate, cx).width(rems(34.)) + }); + } +} + +impl PickerDelegate for NewPathDelegate { + type ListItem = ui::ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = ix; + cx.notify(); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let query = query.trim().trim_start_matches('/'); + let (dir, suffix) = if let Some(index) = query.rfind('/') { + let suffix = if index + 1 < query.len() { + Some(query[index + 1..].to_string()) + } else { + None + }; + (query[0..index].to_string(), suffix) + } else { + (query.to_string(), None) + }; + + let worktrees = self + .project + .read(cx) + .visible_worktrees(cx) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() + .map(|worktree| { + let worktree = worktree.read(cx); + PathMatchCandidateSet { + snapshot: worktree.snapshot(), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), + include_root_name, + directories_only: true, + } + }) + .collect::>(); + + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + + let cancel_flag = self.cancel_flag.clone(); + let query = query.to_string(); + let prefix = dir.clone(); + cx.spawn(|picker, mut cx| async move { + let matches = fuzzy::match_path_sets( + candidate_sets.as_slice(), + &dir, + None, + false, + 100, + &cancel_flag, + cx.background_executor().clone(), + ) + .await; + let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); + if did_cancel { + return; + } + picker + .update(&mut cx, |picker, cx| { + picker + .delegate + .set_search_matches(query, prefix, suffix, matches, cx) + }) + .log_err(); + }) + } + + fn confirm_update_query(&mut self, cx: &mut ViewContext>) -> Option { + let m = self.matches.get(self.selected_index)?; + if m.is_dir(self.project.read(cx), cx) { + let path = m.relative_path(); + self.last_selected_dir = Some(path.clone()); + Some(format!("{}/", path)) + } else { + None + } + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + let Some(m) = self.matches.get(self.selected_index) else { + return; + }; + + let exists = m.entry(self.project.read(cx), cx).is_some(); + if exists { + self.should_dismiss = false; + let answer = cx.prompt( + gpui::PromptLevel::Destructive, + &format!("{} already exists. Do you want to replace it?", m.relative_path()), + Some( + "A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.", + ), + &["Replace", "Cancel"], + ); + let m = m.clone(); + cx.spawn(|picker, mut cx| async move { + let answer = answer.await.ok(); + picker + .update(&mut cx, |picker, cx| { + picker.delegate.should_dismiss = true; + if answer != Some(0) { + return; + } + if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) { + if let Some(tx) = picker.delegate.tx.take() { + tx.send(Some(path)).ok(); + } + } + cx.emit(gpui::DismissEvent); + }) + .ok(); + }) + .detach(); + return; + } + + if let Some(path) = m.project_path(self.project.read(cx), cx) { + if let Some(tx) = self.tx.take() { + tx.send(Some(path)).ok(); + } + } + cx.emit(gpui::DismissEvent); + } + + fn should_dismiss(&self) -> bool { + self.should_dismiss + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + if let Some(tx) = self.tx.take() { + tx.send(None).ok(); + } + cx.emit(gpui::DismissEvent) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let m = self.matches.get(ix)?; + + Some( + ListItem::new(ix) + .spacing(ListItemSpacing::Sparse) + .inset(true) + .selected(selected) + .child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))), + ) + } + + fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString { + "Type a path...".into() + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + Arc::from("[directory/]filename.ext") + } +} + +impl NewPathDelegate { + fn set_search_matches( + &mut self, + query: String, + prefix: String, + suffix: Option, + matches: Vec, + cx: &mut ViewContext>, + ) { + cx.notify(); + if query.is_empty() { + self.matches = vec![]; + return; + } + + let mut directory_exists = false; + + self.matches = matches + .into_iter() + .map(|m| { + if m.path.as_ref().to_string_lossy() == prefix { + directory_exists = true + } + Match { + path_match: Some(m), + suffix: suffix.clone(), + } + }) + .collect(); + + if !directory_exists { + if suffix.is_none() + || self + .last_selected_dir + .as_ref() + .is_some_and(|d| query.starts_with(d)) + { + self.matches.insert( + 0, + Match { + path_match: None, + suffix: Some(query.clone()), + }, + ) + } else { + self.matches.push(Match { + path_match: None, + suffix: Some(query.clone()), + }) + } + } + } +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 23cd8bce52..2b3b901ebf 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -693,7 +693,7 @@ pub struct PathPromptOptions { } /// What kind of prompt styling to show -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum PromptLevel { /// A prompt that is shown when the user should be notified of something Info, @@ -703,6 +703,10 @@ pub enum PromptLevel { /// A prompt that is shown when a critical problem has occurred Critical, + + /// A prompt that is shown when asking the user to confirm a potentially destructive action + /// (overwriting a file for example) + Destructive, } /// The style of the cursor (pointer) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0780d89e7b..7b57c576f1 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -904,7 +904,7 @@ impl PlatformWindow for MacWindow { let alert_style = match level { PromptLevel::Info => 1, PromptLevel::Warning => 0, - PromptLevel::Critical => 2, + PromptLevel::Critical | PromptLevel::Destructive => 2, }; let _: () = msg_send![alert, setAlertStyle: alert_style]; let _: () = msg_send![alert, setMessageText: ns_string(msg)]; @@ -919,10 +919,17 @@ impl PlatformWindow for MacWindow { { let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; let _: () = msg_send![button, setTag: ix as NSInteger]; + if level == PromptLevel::Destructive && answer != &"Cancel" { + let _: () = msg_send![button, setHasDestructiveAction: YES]; + } } if let Some((ix, answer)) = latest_non_cancel_label { let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; let _: () = msg_send![button, setTag: ix as NSInteger]; + let _: () = msg_send![button, setHasDestructiveAction: YES]; + if level == PromptLevel::Destructive { + let _: () = msg_send![button, setHasDestructiveAction: YES]; + } } let (done_tx, done_rx) = oneshot::channel(); diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index f4cf1ded44..e05904426a 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -1455,7 +1455,7 @@ impl PlatformWindow for WindowsWindow { title = windows::core::w!("Warning"); main_icon = TD_WARNING_ICON; } - crate::PromptLevel::Critical => { + crate::PromptLevel::Critical | crate::PromptLevel::Destructive => { title = windows::core::w!("Critical"); main_icon = TD_ERROR_ICON; } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 4a7c78d751..6d7e3ac94e 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -628,6 +628,13 @@ impl From<&TextStyle> for HighlightStyle { } impl HighlightStyle { + /// Create a highlight style with just a color + pub fn color(color: Hsla) -> Self { + Self { + color: Some(color), + ..Default::default() + } + } /// Blend this highlight style with another. /// Non-continuous properties, like font_weight and font_style, are overwritten. pub fn highlight(&mut self, other: HighlightStyle) { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index f3fbc4f111..da0a85a8e0 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -79,11 +79,18 @@ pub trait PickerDelegate: Sized + 'static { false } + fn confirm_update_query(&mut self, _cx: &mut ViewContext>) -> Option { + None + } + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); /// Instead of interacting with currently selected entry, treats editor input literally, /// performing some kind of action on it. fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext>) {} fn dismissed(&mut self, cx: &mut ViewContext>); + fn should_dismiss(&self) -> bool { + true + } fn selected_as_query(&self) -> Option { None } @@ -267,8 +274,10 @@ impl Picker { } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - self.delegate.dismissed(cx); - cx.emit(DismissEvent); + if self.delegate.should_dismiss() { + self.delegate.dismissed(cx); + cx.emit(DismissEvent); + } } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -280,7 +289,7 @@ impl Picker { self.confirm_on_update = Some(false) } else { self.pending_update_matches.take(); - self.delegate.confirm(false, cx); + self.do_confirm(false, cx); } } @@ -292,7 +301,7 @@ impl Picker { { self.confirm_on_update = Some(true) } else { - self.delegate.confirm(true, cx); + self.do_confirm(true, cx); } } @@ -311,7 +320,16 @@ impl Picker { cx.stop_propagation(); cx.prevent_default(); self.delegate.set_selected_index(ix, cx); - self.delegate.confirm(secondary, cx); + self.do_confirm(secondary, cx) + } + + fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext) { + if let Some(update_query) = self.delegate.confirm_update_query(cx) { + self.set_query(update_query, cx); + self.delegate.set_selected_index(0, cx); + } else { + self.delegate.confirm(secondary, cx) + } } fn on_input_editor_event( @@ -385,7 +403,7 @@ impl Picker { self.scroll_to_item_index(index); self.pending_update_matches = None; if let Some(secondary) = self.confirm_on_update.take() { - self.delegate.confirm(secondary, cx); + self.do_confirm(secondary, cx); } cx.notify(); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fa4c8d88d3..352ffa01e6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -32,6 +32,7 @@ use futures::{ stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; +use fuzzy::CharBag; use git::{blame::Blame, repository::GitRepository}; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ @@ -370,6 +371,22 @@ pub struct ProjectPath { pub path: Arc, } +impl ProjectPath { + pub fn from_proto(p: proto::ProjectPath) -> Self { + Self { + worktree_id: WorktreeId::from_proto(p.worktree_id), + path: Arc::from(PathBuf::from(p.path)), + } + } + + pub fn to_proto(&self) -> proto::ProjectPath { + proto::ProjectPath { + worktree_id: self.worktree_id.to_proto(), + path: self.path.to_string_lossy().to_string(), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { pub position: language::Anchor, @@ -2189,33 +2206,37 @@ impl Project { let path = file.path.clone(); worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx), - Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx), + Worktree::Remote(worktree) => worktree.save_buffer(buffer, None, cx), }) } pub fn save_buffer_as( &mut self, buffer: Model, - abs_path: PathBuf, + path: ProjectPath, cx: &mut ModelContext, ) -> Task> { - let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx); let old_file = File::from_dyn(buffer.read(cx).file()) .filter(|f| f.is_local()) .cloned(); + let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) else { + return Task::ready(Err(anyhow!("worktree does not exist"))); + }; + cx.spawn(move |this, mut cx| async move { if let Some(old_file) = &old_file { this.update(&mut cx, |this, cx| { this.unregister_buffer_from_language_servers(&buffer, old_file, cx); })?; } - let (worktree, path) = worktree_task.await?; worktree .update(&mut cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { - worktree.save_buffer(buffer.clone(), path.into(), true, cx) + worktree.save_buffer(buffer.clone(), path.path, true, cx) + } + Worktree::Remote(worktree) => { + worktree.save_buffer(buffer.clone(), Some(path.to_proto()), cx) } - Worktree::Remote(_) => panic!("cannot remote buffers as new files"), })? .await?; @@ -8676,8 +8697,17 @@ impl Project { .await?; let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?; - this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))? + if let Some(new_path) = envelope.payload.new_path { + let new_path = ProjectPath::from_proto(new_path); + this.update(&mut cx, |this, cx| { + this.save_buffer_as(buffer.clone(), new_path, cx) + })? .await?; + } else { + this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))? + .await?; + } + buffer.update(&mut cx, |buffer, _| proto::BufferSaved { project_id, buffer_id: buffer_id.into(), @@ -10414,6 +10444,7 @@ pub struct PathMatchCandidateSet { pub snapshot: Snapshot, pub include_ignored: bool, pub include_root_name: bool, + pub directories_only: bool, } impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { @@ -10443,7 +10474,11 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet { fn candidates(&'a self, start: usize) -> Self::Candidates { PathMatchCandidateSetIter { - traversal: self.snapshot.files(self.include_ignored, start), + traversal: if self.directories_only { + self.snapshot.directories(self.include_ignored, start) + } else { + self.snapshot.files(self.include_ignored, start) + }, } } } @@ -10456,15 +10491,16 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> { type Item = fuzzy::PathMatchCandidate<'a>; fn next(&mut self) -> Option { - self.traversal.next().map(|entry| { - if let EntryKind::File(char_bag) = entry.kind { - fuzzy::PathMatchCandidate { - path: &entry.path, - char_bag, - } - } else { - unreachable!() - } + self.traversal.next().map(|entry| match entry.kind { + EntryKind::Dir => fuzzy::PathMatchCandidate { + path: &entry.path, + char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()), + }, + EntryKind::File(char_bag) => fuzzy::PathMatchCandidate { + path: &entry.path, + char_bag, + }, + EntryKind::UnloadedDir | EntryKind::PendingDir => unreachable!(), }) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 188fb50b53..275b2f3f97 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2942,7 +2942,12 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) { }); project .update(cx, |project, cx| { - project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx) + let worktree_id = project.worktrees().next().unwrap().read(cx).id(); + let path = ProjectPath { + worktree_id, + path: Arc::from(Path::new("file1.rs")), + }; + project.save_buffer_as(buffer.clone(), path, cx) }) .await .unwrap(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ddbd8429ff..286f058d1e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -887,7 +887,7 @@ impl ProjectPanel { let answer = (!action.skip_prompt).then(|| { cx.prompt( - PromptLevel::Info, + PromptLevel::Destructive, &format!("Delete {file_name:?}?"), None, &["Delete", "Cancel"], diff --git a/crates/recent_projects/src/remote_projects.rs b/crates/recent_projects/src/remote_projects.rs index 447b22771c..61900efef7 100644 --- a/crates/recent_projects/src/remote_projects.rs +++ b/crates/recent_projects/src/remote_projects.rs @@ -216,7 +216,7 @@ impl RemoteProjects { fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext) { let answer = cx.prompt( - gpui::PromptLevel::Info, + gpui::PromptLevel::Destructive, "Are you sure?", Some("This will delete the dev server and all of its remote projects."), &["Delete", "Cancel"], diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index b3014d1748..cf75750e15 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -769,6 +769,12 @@ message SaveBuffer { uint64 project_id = 1; uint64 buffer_id = 2; repeated VectorClockEntry version = 3; + optional ProjectPath new_path = 4; +} + +message ProjectPath { + uint64 worktree_id = 1; + string path = 2; } message BufferSaved { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 65ca55c5b7..111a0ace61 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -19,14 +19,14 @@ use gpui::{ WeakModel, WeakView, WhiteSpace, WindowContext, }; use menu::Confirm; -use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project}; +use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath}; use settings::Settings; use smol::stream::StreamExt; use std::{ any::{Any, TypeId}, mem, ops::{Not, Range}, - path::{Path, PathBuf}, + path::Path, }; use theme::ThemeSettings; use ui::{ @@ -439,7 +439,7 @@ impl Item for ProjectSearchView { fn save_as( &mut self, _: Model, - _: PathBuf, + _: ProjectPath, _: &mut ViewContext, ) -> Task> { unreachable!("save_as should not have been called") diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 876f584672..0408a2e6c9 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -50,38 +50,49 @@ impl LabelCommon for HighlightedLabel { } } +pub fn highlight_ranges( + text: &str, + indices: &Vec, + style: HighlightStyle, +) -> Vec<(Range, HighlightStyle)> { + let mut highlight_indices = indices.iter().copied().peekable(); + let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); + + while let Some(start_ix) = highlight_indices.next() { + let mut end_ix = start_ix; + + loop { + end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8(); + if let Some(&next_ix) = highlight_indices.peek() { + if next_ix == end_ix { + end_ix = next_ix; + highlight_indices.next(); + continue; + } + } + break; + } + + highlights.push((start_ix..end_ix, style)); + } + + highlights +} + impl RenderOnce for HighlightedLabel { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let highlight_color = cx.theme().colors().text_accent; - let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); + let highlights = highlight_ranges( + &self.label, + &self.highlight_indices, + HighlightStyle { + color: Some(highlight_color), + ..Default::default() + }, + ); - while let Some(start_ix) = highlight_indices.next() { - let mut end_ix = start_ix; - - loop { - end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8(); - if let Some(&next_ix) = highlight_indices.peek() { - if next_ix == end_ix { - end_ix = next_ix; - highlight_indices.next(); - continue; - } - } - break; - } - - highlights.push(( - start_ix..end_ix, - HighlightStyle { - color: Some(highlight_color), - ..Default::default() - }, - )); - } - - let mut text_style = cx.text_style().clone(); + let mut text_style = cx.text_style(); text_style.color = self.base.color.color(cx); self.base diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 15184a9d3b..e45c6ffe13 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -26,7 +26,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::PathBuf, rc::Rc, sync::Arc, time::Duration, @@ -196,7 +195,7 @@ pub trait Item: FocusableView + EventEmitter { fn save_as( &mut self, _project: Model, - _abs_path: PathBuf, + _path: ProjectPath, _cx: &mut ViewContext, ) -> Task> { unimplemented!("save_as() must be implemented if can_save() returns true") @@ -309,7 +308,7 @@ pub trait ItemHandle: 'static + Send { fn save_as( &self, project: Model, - abs_path: PathBuf, + path: ProjectPath, cx: &mut WindowContext, ) -> Task>; fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; @@ -647,10 +646,10 @@ impl ItemHandle for View { fn save_as( &self, project: Model, - abs_path: PathBuf, + path: ProjectPath, cx: &mut WindowContext, ) -> Task> { - self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) + self.update(cx, |item, cx| item.save_as(project, path, cx)) } fn reload(&self, project: Model, cx: &mut WindowContext) -> Task> { @@ -1126,7 +1125,7 @@ pub mod test { fn save_as( &mut self, _: Model, - _: std::path::PathBuf, + _: ProjectPath, _: &mut ViewContext, ) -> Task> { self.save_as_count += 1; diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index c7ea762e15..2757b6e561 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -263,7 +263,7 @@ impl Render for LanguageServerPrompt { PromptLevel::Warning => { Some(DiagnosticSeverity::WARNING) } - PromptLevel::Critical => { + PromptLevel::Critical | PromptLevel::Destructive => { Some(DiagnosticSeverity::ERROR) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c9027f2c90..59c208924a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -26,7 +26,7 @@ use std::{ any::Any, cmp, fmt, mem, ops::ControlFlow, - path::{Path, PathBuf}, + path::PathBuf, rc::Rc, sync::{ atomic::{AtomicUsize, Ordering}, @@ -1322,14 +1322,10 @@ impl Pane { pane.update(cx, |_, cx| item.save(should_format, project, cx))? .await?; } else if can_save_as { - let start_abs_path = project - .update(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next()?; - Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - })? - .unwrap_or_else(|| Path::new("").into()); - - let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?; + let abs_path = pane.update(cx, |pane, cx| { + pane.workspace + .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx)) + })??; if let Some(abs_path) = abs_path.await.ok().flatten() { pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))? .await?; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 98cddd8d25..25a20ec0ce 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -544,6 +544,10 @@ pub enum OpenVisible { OnlyDirectories, } +type PromptForNewPath = Box< + dyn Fn(&mut Workspace, &mut ViewContext) -> oneshot::Receiver>, +>; + /// Collects everything project-related for a certain window opened. /// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`. /// @@ -585,6 +589,7 @@ pub struct Workspace { bounds: Bounds, centered_layout: bool, bounds_save_task_queued: Option>, + on_prompt_for_new_path: Option, } impl EventEmitter for Workspace {} @@ -875,6 +880,7 @@ impl Workspace { bounds: Default::default(), centered_layout: false, bounds_save_task_queued: None, + on_prompt_for_new_path: None, } } @@ -1223,6 +1229,59 @@ impl Workspace { cx.notify(); } + pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) { + self.on_prompt_for_new_path = Some(prompt) + } + + pub fn prompt_for_new_path( + &mut self, + cx: &mut ViewContext, + ) -> oneshot::Receiver> { + if let Some(prompt) = self.on_prompt_for_new_path.take() { + let rx = prompt(self, cx); + self.on_prompt_for_new_path = Some(prompt); + rx + } else { + let start_abs_path = self + .project + .update(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next()?; + Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) + }) + .unwrap_or_else(|| Path::new("").into()); + + let (tx, rx) = oneshot::channel(); + let abs_path = cx.prompt_for_new_path(&start_abs_path); + cx.spawn(|this, mut cx| async move { + let abs_path = abs_path.await?; + let project_path = abs_path.and_then(|abs_path| { + this.update(&mut cx, |this, cx| { + this.project.update(cx, |project, cx| { + project.find_or_create_local_worktree(abs_path, true, cx) + }) + }) + .ok() + }); + + if let Some(project_path) = project_path { + let (worktree, path) = project_path.await?; + let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?; + tx.send(Some(ProjectPath { + worktree_id, + path: path.into(), + })) + .ok(); + } else { + tx.send(None).ok(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + rx + } + } + pub fn titlebar_item(&self) -> Option { self.titlebar_item.clone() } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 5e4556f3d2..10cbbf5339 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1625,6 +1625,7 @@ impl RemoteWorktree { pub fn save_buffer( &self, buffer_handle: Model, + new_path: Option, cx: &mut ModelContext, ) -> Task> { let buffer = buffer_handle.read(cx); @@ -1637,6 +1638,7 @@ impl RemoteWorktree { .request(proto::SaveBuffer { project_id, buffer_id, + new_path, version: serialize_version(&version), }) .await?; @@ -1911,6 +1913,7 @@ impl Snapshot { fn traverse_from_offset( &self, + include_files: bool, include_dirs: bool, include_ignored: bool, start_offset: usize, @@ -1919,6 +1922,7 @@ impl Snapshot { cursor.seek( &TraversalTarget::Count { count: start_offset, + include_files, include_dirs, include_ignored, }, @@ -1927,6 +1931,7 @@ impl Snapshot { ); Traversal { cursor, + include_files, include_dirs, include_ignored, } @@ -1934,6 +1939,7 @@ impl Snapshot { fn traverse_from_path( &self, + include_files: bool, include_dirs: bool, include_ignored: bool, path: &Path, @@ -1942,17 +1948,22 @@ impl Snapshot { cursor.seek(&TraversalTarget::Path(path), Bias::Left, &()); Traversal { cursor, + include_files, include_dirs, include_ignored, } } pub fn files(&self, include_ignored: bool, start: usize) -> Traversal { - self.traverse_from_offset(false, include_ignored, start) + self.traverse_from_offset(true, false, include_ignored, start) + } + + pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal { + self.traverse_from_offset(false, true, include_ignored, start) } pub fn entries(&self, include_ignored: bool) -> Traversal { - self.traverse_from_offset(true, include_ignored, 0) + self.traverse_from_offset(true, true, include_ignored, 0) } pub fn repositories(&self) -> impl Iterator, &RepositoryEntry)> { @@ -2084,6 +2095,7 @@ impl Snapshot { cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &()); let traversal = Traversal { cursor, + include_files: true, include_dirs: true, include_ignored: true, }; @@ -2103,6 +2115,7 @@ impl Snapshot { cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &()); let mut traversal = Traversal { cursor, + include_files: true, include_dirs, include_ignored, }; @@ -2141,7 +2154,7 @@ impl Snapshot { pub fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { let path = path.as_ref(); - self.traverse_from_path(true, true, path) + self.traverse_from_path(true, true, true, path) .entry() .and_then(|entry| { if entry.path.as_ref() == path { @@ -4532,12 +4545,15 @@ struct TraversalProgress<'a> { } impl<'a> TraversalProgress<'a> { - fn count(&self, include_dirs: bool, include_ignored: bool) -> usize { - match (include_ignored, include_dirs) { - (true, true) => self.count, - (true, false) => self.file_count, - (false, true) => self.non_ignored_count, - (false, false) => self.non_ignored_file_count, + fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize { + match (include_files, include_dirs, include_ignored) { + (true, true, true) => self.count, + (true, true, false) => self.non_ignored_count, + (true, false, true) => self.file_count, + (true, false, false) => self.non_ignored_file_count, + (false, true, true) => self.count - self.file_count, + (false, true, false) => self.non_ignored_count - self.non_ignored_file_count, + (false, false, _) => 0, } } } @@ -4600,6 +4616,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses { pub struct Traversal<'a> { cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>, include_ignored: bool, + include_files: bool, include_dirs: bool, } @@ -4609,6 +4626,7 @@ impl<'a> Traversal<'a> { &TraversalTarget::Count { count: self.end_offset() + 1, include_dirs: self.include_dirs, + include_files: self.include_files, include_ignored: self.include_ignored, }, Bias::Left, @@ -4624,7 +4642,8 @@ impl<'a> Traversal<'a> { &(), ); if let Some(entry) = self.cursor.item() { - if (self.include_dirs || !entry.is_dir()) + if (self.include_files || !entry.is_file()) + && (self.include_dirs || !entry.is_dir()) && (self.include_ignored || !entry.is_ignored) { return true; @@ -4641,13 +4660,13 @@ impl<'a> Traversal<'a> { pub fn start_offset(&self) -> usize { self.cursor .start() - .count(self.include_dirs, self.include_ignored) + .count(self.include_files, self.include_dirs, self.include_ignored) } pub fn end_offset(&self) -> usize { self.cursor .end(&()) - .count(self.include_dirs, self.include_ignored) + .count(self.include_files, self.include_dirs, self.include_ignored) } } @@ -4670,6 +4689,7 @@ enum TraversalTarget<'a> { PathSuccessor(&'a Path), Count { count: usize, + include_files: bool, include_ignored: bool, include_dirs: bool, }, @@ -4688,11 +4708,12 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa } TraversalTarget::Count { count, + include_files, include_dirs, include_ignored, } => Ord::cmp( count, - &cursor_location.count(*include_dirs, *include_ignored), + &cursor_location.count(*include_files, *include_dirs, *include_ignored), ), } } From 9329ef1d7864921979c2554ce18cbce807746ae4 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Fri, 26 Apr 2024 21:34:45 +0200 Subject: [PATCH 093/101] markdown preview: Break up list items into individual blocks (#10852) Fixes a panic related to rendering checkboxes, see #10824. Currently we are rendering a list into a single block, meaning the whole block has to be rendered when it is visible on screen. This would lead to performance problems when a single list block contained a lot of items (especially if it contained checkboxes). This PR splits up list items into separate blocks, meaning only the actual visible list items on screen get rendered, instead of the whole list. A nice side effect of the refactoring is, that you can actually click on individual list items now: https://github.com/zed-industries/zed/assets/53836821/5ef4200c-bd85-4e96-a8bf-e0c8b452f762 Release Notes: - Improved rendering performance of list elements inside the markdown preview --------- Co-authored-by: Remco --- Cargo.lock | 1 + crates/markdown_preview/Cargo.toml | 1 + .../markdown_preview/src/markdown_elements.rs | 21 +- .../markdown_preview/src/markdown_parser.rs | 390 +++++++++--------- .../src/markdown_preview_view.rs | 17 +- .../markdown_preview/src/markdown_renderer.rs | 135 +++--- 6 files changed, 286 insertions(+), 279 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6302e78434..d358f28f84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5902,6 +5902,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", + "collections", "editor", "gpui", "language", diff --git a/crates/markdown_preview/Cargo.toml b/crates/markdown_preview/Cargo.toml index 3a46d2b46d..e1e514a63e 100644 --- a/crates/markdown_preview/Cargo.toml +++ b/crates/markdown_preview/Cargo.toml @@ -17,6 +17,7 @@ test-support = [] [dependencies] anyhow.workspace = true async-recursion.workspace = true +collections.workspace = true editor.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 75dc0bcbf7..08d25216a0 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -8,8 +8,7 @@ use std::{fmt::Display, ops::Range, path::PathBuf}; #[cfg_attr(test, derive(PartialEq))] pub enum ParsedMarkdownElement { Heading(ParsedMarkdownHeading), - /// An ordered or unordered list of items. - List(ParsedMarkdownList), + ListItem(ParsedMarkdownListItem), Table(ParsedMarkdownTable), BlockQuote(ParsedMarkdownBlockQuote), CodeBlock(ParsedMarkdownCodeBlock), @@ -22,7 +21,7 @@ impl ParsedMarkdownElement { pub fn source_range(&self) -> Range { match self { Self::Heading(heading) => heading.source_range.clone(), - Self::List(list) => list.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(), @@ -30,6 +29,10 @@ impl ParsedMarkdownElement { Self::HorizontalRule(range) => range.clone(), } } + + pub fn is_list_item(&self) -> bool { + matches!(self, Self::ListItem(_)) + } } #[derive(Debug)] @@ -38,20 +41,14 @@ pub struct ParsedMarkdown { pub children: Vec, } -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq))] -pub struct ParsedMarkdownList { - pub source_range: Range, - pub children: Vec, -} - #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownListItem { + pub source_range: Range, /// How many indentations deep this item is. pub depth: u16, pub item_type: ParsedMarkdownListItemType, - pub contents: Vec>, + pub content: Vec, } #[derive(Debug)] @@ -129,7 +126,7 @@ impl ParsedMarkdownTableRow { #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownBlockQuote { pub source_range: Range, - pub children: Vec>, + pub children: Vec, } #[derive(Debug)] diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index fc37d3e3f5..27a5e53245 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -1,5 +1,6 @@ use crate::markdown_elements::*; use async_recursion::async_recursion; +use collections::FxHashMap; use gpui::FontWeight; use language::LanguageRegistry; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; @@ -98,20 +99,22 @@ impl<'a> MarkdownParser<'a> { async fn parse_document(mut self) -> Self { while !self.eof() { if let Some(block) = self.parse_block().await { - self.parsed.push(block); + self.parsed.extend(block); } } self } - async fn parse_block(&mut self) -> Option { + #[async_recursion] + async fn parse_block(&mut self) -> Option> { let (current, source_range) = self.current().unwrap(); + let source_range = source_range.clone(); match current { Event::Start(tag) => match tag { Tag::Paragraph => { self.cursor += 1; - let text = self.parse_text(false); - Some(ParsedMarkdownElement::Paragraph(text)) + let text = self.parse_text(false, Some(source_range)); + Some(vec![ParsedMarkdownElement::Paragraph(text)]) } Tag::Heading { level, @@ -122,24 +125,24 @@ impl<'a> MarkdownParser<'a> { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); - Some(ParsedMarkdownElement::Heading(heading)) + Some(vec![ParsedMarkdownElement::Heading(heading)]) } Tag::Table(alignment) => { let alignment = alignment.clone(); self.cursor += 1; let table = self.parse_table(alignment); - Some(ParsedMarkdownElement::Table(table)) + Some(vec![ParsedMarkdownElement::Table(table)]) } Tag::List(order) => { let order = *order; self.cursor += 1; - let list = self.parse_list(1, order).await; - Some(ParsedMarkdownElement::List(list)) + let list = self.parse_list(order).await; + Some(list) } Tag::BlockQuote => { self.cursor += 1; let block_quote = self.parse_block_quote().await; - Some(ParsedMarkdownElement::BlockQuote(block_quote)) + Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)]) } Tag::CodeBlock(kind) => { let language = match kind { @@ -156,7 +159,7 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; let code_block = self.parse_code_block(language).await; - Some(ParsedMarkdownElement::CodeBlock(code_block)) + Some(vec![ParsedMarkdownElement::CodeBlock(code_block)]) } _ => { self.cursor += 1; @@ -166,7 +169,7 @@ impl<'a> MarkdownParser<'a> { Event::Rule => { let source_range = source_range.clone(); self.cursor += 1; - Some(ParsedMarkdownElement::HorizontalRule(source_range)) + Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) } _ => { self.cursor += 1; @@ -175,9 +178,16 @@ impl<'a> MarkdownParser<'a> { } } - fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText { - let (_current, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); + fn parse_text( + &mut self, + should_complete_on_soft_break: bool, + source_range: Option>, + ) -> ParsedMarkdownText { + let source_range = source_range.unwrap_or_else(|| { + self.current() + .map(|(_, range)| range.clone()) + .unwrap_or_default() + }); let mut text = String::new(); let mut bold_depth = 0; @@ -379,7 +389,7 @@ impl<'a> MarkdownParser<'a> { fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); - let text = self.parse_text(true); + let text = self.parse_text(true, None); // Advance past the heading end tag self.cursor += 1; @@ -415,7 +425,8 @@ impl<'a> MarkdownParser<'a> { break; } - let (current, _source_range) = self.current().unwrap(); + let (current, source_range) = self.current().unwrap(); + let source_range = source_range.clone(); match current { Event::Start(Tag::TableHead) | Event::Start(Tag::TableRow) @@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> { } Event::Start(Tag::TableCell) => { self.cursor += 1; - let cell_contents = self.parse_text(false); + let cell_contents = self.parse_text(false, Some(source_range)); current_row.push(cell_contents); } Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { @@ -465,35 +476,53 @@ impl<'a> MarkdownParser<'a> { } } - #[async_recursion] - async fn parse_list(&mut self, depth: u16, order: Option) -> ParsedMarkdownList { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let mut children = vec![]; - let mut inside_list_item = false; - let mut order = order; - let mut task_item = None; + async fn parse_list(&mut self, order: Option) -> Vec { + let (_, list_source_range) = self.previous().unwrap(); - let mut current_list_items: Vec> = vec![]; + let mut items = Vec::new(); + let mut items_stack = vec![Vec::new()]; + let mut depth = 1; + let mut task_item = None; + let mut order = order; + let mut order_stack = Vec::new(); + + let mut insertion_indices = FxHashMap::default(); + let mut source_ranges = FxHashMap::default(); + let mut start_item_range = list_source_range.clone(); while !self.eof() { - let (current, _source_range) = self.current().unwrap(); + let (current, source_range) = self.current().unwrap(); match current { - Event::Start(Tag::List(order)) => { - let order = *order; - self.cursor += 1; + Event::Start(Tag::List(new_order)) => { + if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) { + insertion_indices.insert(depth, items.len()); + } - let inner_list = self.parse_list(depth + 1, order).await; - let block = ParsedMarkdownElement::List(inner_list); - current_list_items.push(Box::new(block)); + // We will use the start of the nested list as the end for the current item's range, + // because we don't care about the hierarchy of list items + if !source_ranges.contains_key(&depth) { + source_ranges.insert(depth, start_item_range.start..source_range.start); + } + + order_stack.push(order); + order = *new_order; + self.cursor += 1; + depth += 1; } Event::End(TagEnd::List(_)) => { + order = order_stack.pop().flatten(); self.cursor += 1; - break; + depth -= 1; + + if depth == 0 { + break; + } } Event::Start(Tag::Item) => { + start_item_range = source_range.clone(); + self.cursor += 1; - inside_list_item = true; + items_stack.push(Vec::new()); // Check for task list marker (`- [ ]` or `- [x]`) if let Some(event) = self.current_event() { @@ -508,17 +537,21 @@ impl<'a> MarkdownParser<'a> { } } - if let Some(event) = self.current_event() { + if let Some((event, range)) = self.current() { // This is a plain list item. // For example `- some text` or `1. [Docs](./docs.md)` if MarkdownParser::is_text_like(event) { - let text = self.parse_text(false); + let text = self.parse_text(false, Some(range.clone())); let block = ParsedMarkdownElement::Paragraph(text); - current_list_items.push(Box::new(block)); + if let Some(content) = items_stack.last_mut() { + content.push(block); + } } else { let block = self.parse_block().await; if let Some(block) = block { - current_list_items.push(Box::new(block)); + if let Some(content) = items_stack.last_mut() { + content.extend(block); + } } } } @@ -543,34 +576,55 @@ impl<'a> MarkdownParser<'a> { order = Some(current + 1); } - let contents = std::mem::replace(&mut current_list_items, vec![]); + if let Some(content) = items_stack.pop() { + let source_range = source_ranges + .remove(&depth) + .unwrap_or(start_item_range.clone()); - children.push(ParsedMarkdownListItem { - contents, - depth, - item_type, - }); + // We need to remove the last character of the source range, because it includes the newline character + let source_range = source_range.start..source_range.end - 1; + let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { + source_range, + content, + depth, + item_type, + }); + + if let Some(index) = insertion_indices.get(&depth) { + items.insert(*index, item); + insertion_indices.remove(&depth); + } else { + items.push(item); + } + } - inside_list_item = false; task_item = None; } _ => { - if !inside_list_item { + if depth == 0 { break; } - + // This can only happen if a list item starts with more then one paragraph, + // or the list item contains blocks that should be rendered after the nested list items let block = self.parse_block().await; if let Some(block) = block { - current_list_items.push(Box::new(block)); + if let Some(items_stack) = items_stack.last_mut() { + // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item + if !insertion_indices.contains_key(&depth) { + items_stack.extend(block); + continue; + } + } + + // Otherwise we need to insert the block after all the nested items + // that have been parsed so far + items.extend(block); } } } } - ParsedMarkdownList { - source_range, - children, - } + items } #[async_recursion] @@ -579,13 +633,13 @@ impl<'a> MarkdownParser<'a> { let source_range = source_range.clone(); let mut nested_depth = 1; - let mut children: Vec> = vec![]; + let mut children: Vec = vec![]; while !self.eof() { let block = self.parse_block().await; if let Some(block) = block { - children.push(Box::new(block)); + children.extend(block); } else { break; } @@ -674,7 +728,6 @@ mod tests { use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher}; use pretty_assertions::assert_eq; - use ParsedMarkdownElement::*; use ParsedMarkdownListItemType::*; async fn parse(input: &str) -> ParsedMarkdown { @@ -688,9 +741,9 @@ mod tests { assert_eq!( parsed.children, vec![ - h1(text("Heading one", 0..14), 0..14), - h2(text("Heading two", 14..29), 14..29), - h3(text("Heading three", 29..46), 29..46), + h1(text("Heading one", 2..13), 0..14), + h2(text("Heading two", 17..28), 14..29), + h3(text("Heading three", 33..46), 29..46), ] ); } @@ -711,7 +764,7 @@ mod tests { assert_eq!( parsed.children, - vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),] + vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),] ); } @@ -881,14 +934,11 @@ Some other content assert_eq!( parsed.children, - vec![list( - vec![ - list_item(1, Unordered, vec![p("Item 1", 0..9)]), - list_item(1, Unordered, vec![p("Item 2", 9..18)]), - list_item(1, Unordered, vec![p("Item 3", 18..27)]), - ], - 0..27 - ),] + vec![ + list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), + list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), + list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]), + ], ); } @@ -904,13 +954,10 @@ Some other content assert_eq!( parsed.children, - vec![list( - vec![ - list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]), - list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]), - ], - 0..25 - ),] + vec![ + list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]), + list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]), + ], ); } @@ -927,13 +974,10 @@ Some other content assert_eq!( parsed.children, - vec![list( - vec![ - list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]), - list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]), - ], - 0..27 - ),] + vec![ + list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]), + list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]), + ], ); } @@ -965,84 +1009,21 @@ Some other content assert_eq!( parsed.children, vec![ - list( - vec![ - list_item(1, Unordered, vec![p("Item 1", 0..9)]), - list_item(1, Unordered, vec![p("Item 2", 9..18)]), - list_item(1, Unordered, vec![p("Item 3", 18..28)]), - ], - 0..28 - ), - list( - vec![ - list_item(1, Ordered(1), vec![p("Hello", 28..37)]), - list_item( - 1, - Ordered(2), - vec![ - p("Two", 37..56), - list( - vec![list_item(2, Ordered(1), vec![p("Three", 47..56)]),], - 47..56 - ), - ] - ), - list_item(1, Ordered(3), vec![p("Four", 56..64)]), - list_item(1, Ordered(4), vec![p("Five", 64..73)]), - ], - 28..73 - ), - list( - vec![ - list_item( - 1, - Unordered, - vec![ - p("First", 73..155), - list( - vec![ - list_item( - 2, - Ordered(1), - vec![ - p("Hello", 83..141), - list( - vec![list_item( - 3, - Ordered(1), - vec![ - p("Goodbyte", 97..141), - list( - vec![ - list_item( - 4, - Unordered, - vec![p("Inner", 117..125)] - ), - list_item( - 4, - Unordered, - vec![p("Inner", 133..141)] - ), - ], - 117..141 - ) - ] - ),], - 97..141 - ) - ] - ), - list_item(2, Ordered(2), vec![p("Goodbyte", 143..155)]), - ], - 83..155 - ) - ] - ), - list_item(1, Unordered, vec![p("Last", 155..162)]), - ], - 73..162 - ), + list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), + list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), + list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]), + list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]), + list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]), + list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]), + list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]), + list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]), + list_item(73..82, 1, Unordered, vec![p("First", 75..80)]), + list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]), + list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]), + list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]), + list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]), + list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]), + list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]), ] ); } @@ -1053,23 +1034,49 @@ Some other content "\ * This is a list item with two paragraphs. - This is the second paragraph in the list item.", + This is the second paragraph in the list item. +", ) .await; assert_eq!( parsed.children, - vec![list( - vec![list_item( - 1, - Unordered, - vec![ - p("This is a list item with two paragraphs.", 4..45), - p("This is the second paragraph in the list item.", 50..96) - ], - ),], + vec![list_item( 0..96, - ),] + 1, + Unordered, + vec![ + p("This is a list item with two paragraphs.", 4..44), + p("This is the second paragraph in the list item.", 50..97) + ], + ),], + ); + } + + #[gpui::test] + async fn test_nested_list_with_paragraph_inside() { + let parsed = parse( + "\ +1. a + 1. b + 1. c + + text + + 1. d +", + ) + .await; + + assert_eq!( + parsed.children, + vec![ + list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],), + list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],), + list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],), + p("text", 32..37), + list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],), + ], ); } @@ -1086,14 +1093,11 @@ Some other content assert_eq!( parsed.children, - vec![list( - vec![ - list_item(1, Unordered, vec![p("code", 0..9)],), - list_item(1, Unordered, vec![p("bold", 9..20)]), - list_item(1, Unordered, vec![p("link", 20..50)],) - ], - 0..50, - ),] + 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)],) + ], ); } @@ -1127,7 +1131,7 @@ Some other content parsed.children, vec![block_quote( vec![ - h1(text("Heading", 2..12), 2..12), + h1(text("Heading", 4..11), 2..12), p("More text", 14..26), p("More text", 30..40) ], @@ -1157,7 +1161,7 @@ More text block_quote( vec![ p("A", 2..4), - block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14), + block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14), p("C", 18..20) ], 0..20 @@ -1279,7 +1283,7 @@ fn main() { ) -> ParsedMarkdownElement { ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote { source_range, - children: children.into_iter().map(Box::new).collect(), + children, }) } @@ -1297,26 +1301,18 @@ fn main() { }) } - fn list( - children: Vec, - source_range: Range, - ) -> ParsedMarkdownElement { - List(ParsedMarkdownList { - source_range, - children, - }) - } - fn list_item( + source_range: Range, depth: u16, item_type: ParsedMarkdownListItemType, - contents: Vec, - ) -> ParsedMarkdownListItem { - ParsedMarkdownListItem { + content: Vec, + ) -> ParsedMarkdownElement { + ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { + source_range, item_type, depth, - contents: contents.into_iter().map(Box::new).collect(), - } + content, + }) } fn table( diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 4f8bb78475..e57e4da729 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -15,6 +15,7 @@ use ui::prelude::*; use workspace::item::{Item, ItemHandle, TabContentParams}; use workspace::{Pane, Workspace}; +use crate::markdown_elements::ParsedMarkdownElement; use crate::OpenPreviewToTheSide; use crate::{ markdown_elements::ParsedMarkdown, @@ -180,9 +181,14 @@ impl MarkdownPreviewView { let block = contents.children.get(ix).unwrap(); let rendered_block = render_markdown_block(block, &mut render_cx); + let should_apply_padding = Self::should_apply_padding_between( + block, + contents.children.get(ix + 1), + ); + div() .id(ix) - .pb_3() + .when(should_apply_padding, |this| this.pb_3()) .group("markdown-block") .on_click(cx.listener(move |this, event: &ClickEvent, cx| { if event.down.click_count == 2 { @@ -404,7 +410,7 @@ impl MarkdownPreviewView { let Range { start, end } = block.source_range(); // Check if the cursor is between the last block and the current block - if last_end > cursor && cursor < start { + if last_end <= cursor && cursor < start { block_index = Some(i.saturating_sub(1)); break; } @@ -423,6 +429,13 @@ impl MarkdownPreviewView { block_index.unwrap_or_default() } + + fn should_apply_padding_between( + current_block: &ParsedMarkdownElement, + next_block: Option<&ParsedMarkdownElement>, + ) -> bool { + !(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false)) + } } impl FocusableView for MarkdownPreviewView { diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index da86e772e5..4de654e46a 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,7 +1,8 @@ use crate::markdown_elements::{ HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, - ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType, - ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText, + ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, + ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, + ParsedMarkdownTableRow, ParsedMarkdownText, }; use gpui::{ div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId, @@ -110,7 +111,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte match block { Paragraph(text) => render_markdown_paragraph(text, cx), Heading(heading) => render_markdown_heading(heading, cx), - List(list) => render_markdown_list(list, cx), + ListItem(list_item) => render_markdown_list_item(list_item, cx), Table(table) => render_markdown_table(table, cx), BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), CodeBlock(code_block) => render_markdown_code_block(code_block, cx), @@ -146,79 +147,77 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex .into_any() } -fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement { +fn render_markdown_list_item( + parsed: &ParsedMarkdownListItem, + cx: &mut RenderContext, +) -> AnyElement { use ParsedMarkdownListItemType::*; - let mut items = vec![]; - for item in &parsed.children { - let padding = rems((item.depth - 1) as f32 * 0.25); + let padding = rems((parsed.depth - 1) as f32); - let bullet = match &item.item_type { - Ordered(order) => format!("{}.", order).into_any_element(), - Unordered => "•".into_any_element(), - Task(checked, range) => div() - .id(cx.next_id(range)) - .mt(px(3.)) - .child( - Checkbox::new( - "checkbox", - if *checked { - Selection::Selected - } else { - Selection::Unselected - }, - ) - .when_some( - cx.checkbox_clicked_callback.clone(), - |this, callback| { - this.on_click({ - let range = range.clone(); - move |selection, cx| { - let checked = match selection { - Selection::Selected => true, - Selection::Unselected => false, - _ => return, - }; - - if cx.modifiers().secondary() { - callback(checked, range.clone(), cx); - } - } - }) - }, - ), + let bullet = match &parsed.item_type { + Ordered(order) => format!("{}.", order).into_any_element(), + Unordered => "•".into_any_element(), + Task(checked, range) => div() + .id(cx.next_id(range)) + .mt(px(3.)) + .child( + Checkbox::new( + "checkbox", + if *checked { + Selection::Selected + } else { + Selection::Unselected + }, ) - .hover(|s| s.cursor_pointer()) - .tooltip(|cx| { - let secondary_modifier = Keystroke { - key: "".to_string(), - modifiers: Modifiers::secondary_key(), - ime_key: None, - }; - Tooltip::text( - format!("{}-click to toggle the checkbox", secondary_modifier), - cx, - ) - }) - .into_any_element(), - }; - let bullet = div().mr_2().child(bullet); + .when_some( + cx.checkbox_clicked_callback.clone(), + |this, callback| { + this.on_click({ + let range = range.clone(); + move |selection, cx| { + let checked = match selection { + Selection::Selected => true, + Selection::Unselected => false, + _ => return, + }; - let contents: Vec = item - .contents - .iter() - .map(|c| render_markdown_block(c.as_ref(), cx)) - .collect(); + if cx.modifiers().secondary() { + callback(checked, range.clone(), cx); + } + } + }) + }, + ), + ) + .hover(|s| s.cursor_pointer()) + .tooltip(|cx| { + let secondary_modifier = Keystroke { + key: "".to_string(), + modifiers: Modifiers::secondary_key(), + ime_key: None, + }; + Tooltip::text( + format!("{}-click to toggle the checkbox", secondary_modifier), + cx, + ) + }) + .into_any_element(), + }; + let bullet = div().mr_2().child(bullet); - let item = h_flex() - .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding))) - .items_start() - .children(vec![bullet, div().children(contents).pr_4().w_full()]); + let contents: Vec = parsed + .content + .iter() + .map(|c| render_markdown_block(c, cx)) + .collect(); - items.push(item); - } + let item = h_flex() + .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding))) + .items_start() + .children(vec![bullet, div().children(contents).pr_4().w_full()]); - cx.with_common_p(div()).children(items).into_any() + cx.with_common_p(item).into_any() } fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { From e0644de90e22186e847415c8a1fc26c93f9d738c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 26 Apr 2024 14:04:18 -0600 Subject: [PATCH 094/101] Fix panic in Diagnostics (#11066) cc @maxbrunsfeld Release Notes: - Fixed a panic in populating diagnostics --- crates/diagnostics/src/diagnostics.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d06ff824fb..0fbacf4743 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -859,20 +859,25 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { }) } -fn compare_diagnostics( - lhs: &DiagnosticEntry, - rhs: &DiagnosticEntry, +fn compare_diagnostics( + old: &DiagnosticEntry, + new: &DiagnosticEntry, snapshot: &language::BufferSnapshot, ) -> Ordering { - lhs.range + use language::ToOffset; + // The old diagnostics may point to a previously open Buffer for this file. + if !old.range.start.is_valid(snapshot) { + return Ordering::Greater; + } + old.range .start .to_offset(snapshot) - .cmp(&rhs.range.start.to_offset(snapshot)) + .cmp(&new.range.start.to_offset(snapshot)) .then_with(|| { - lhs.range + old.range .end .to_offset(snapshot) - .cmp(&rhs.range.end.to_offset(snapshot)) + .cmp(&new.range.end.to_offset(snapshot)) }) - .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message)) + .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message)) } From b1eb0291dc4eb1ed530deb73ac86f59d78d6b145 Mon Sep 17 00:00:00 2001 From: DissolveDZ <68782699+DissolveDZ@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:04:25 +0200 Subject: [PATCH 095/101] Re-add README.md which might have been deleted by mistake (#11067) Release Notes: - N/A --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index e69de29bb2..1c17a950fd 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,51 @@ +# Zed + +[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) + +Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). + +## Installation + +You can [download](https://zed.dev/download) Zed today for macOS (v10.15+). + +Support for additional platforms is on our [roadmap](https://zed.dev/roadmap): + +- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015)) +- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394)) +- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396)) + +For macOS users, you can also install Zed using [Homebrew](https://brew.sh/): + +```sh +brew install --cask zed +``` + +Alternatively, to install the Preview release: + +```sh +brew tap homebrew/cask-versions +brew install zed-preview +``` + +## Developing Zed + +- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md) +- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md) +- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md) +- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md) + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed. + +Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles. + +## Licensing + +License information for third party dependencies must be correctly provided for CI to pass. + +We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following: + +- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml. +- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`. +- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration). From 70d03e484135ef56d5f6a308bab3b70e5f468f82 Mon Sep 17 00:00:00 2001 From: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:53:49 -0300 Subject: [PATCH 096/101] x11: Fix window close (#11008) Fixes https://github.com/zed-industries/zed/issues/10483 on X11 Also calls the `should_close` callback before closing the window (needed for the "Do you want to save?" dialog). Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/client.rs | 48 ++++--- crates/gpui/src/platform/linux/x11/window.rs | 132 +++++++++++++------ 2 files changed, 123 insertions(+), 57 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index bc1a1b527e..d4a0044f79 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; use std::ops::Deref; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use std::time::{Duration, Instant}; use calloop::{EventLoop, LoopHandle}; @@ -23,10 +23,10 @@ use crate::platform::linux::LinuxClient; use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels, - PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, + PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; -use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms}; +use super::{super::SCROLL_LINES, X11Display, X11WindowStatePtr, XcbAtoms}; use super::{button_of_key, modifiers_from_state}; use crate::platform::linux::is_within_click_distance; use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; @@ -36,12 +36,12 @@ use calloop::{ }; pub(crate) struct WindowRef { - window: X11Window, + window: X11WindowStatePtr, refresh_event_token: RegistrationToken, } impl Deref for WindowRef { - type Target = X11Window; + type Target = X11WindowStatePtr; fn deref(&self) -> &Self::Target { &self.window @@ -68,6 +68,24 @@ pub struct X11ClientState { pub(crate) primary: X11ClipboardContext, } +#[derive(Clone)] +pub struct X11ClientStatePtr(pub Weak>); + +impl X11ClientStatePtr { + pub fn drop_window(&self, x_window: u32) { + let client = X11Client(self.0.upgrade().expect("client already dropped")); + let mut state = client.0.borrow_mut(); + + if let Some(window_ref) = state.windows.remove(&x_window) { + state.loop_handle.remove(window_ref.refresh_event_token); + } + + if state.windows.is_empty() { + state.common.signal.stop(); + } + } +} + #[derive(Clone)] pub(crate) struct X11Client(Rc>); @@ -171,7 +189,7 @@ impl X11Client { }))) } - fn get_window(&self, win: xproto::Window) -> Option { + fn get_window(&self, win: xproto::Window) -> Option { let state = self.0.borrow(); state .windows @@ -182,18 +200,16 @@ impl X11Client { fn handle_event(&self, event: Event) -> Option<()> { match event { Event::ClientMessage(event) => { + let window = self.get_window(event.window)?; let [atom, ..] = event.data.as_data32(); let mut state = self.0.borrow_mut(); if atom == state.atoms.WM_DELETE_WINDOW { - // window "x" button clicked by user, we gracefully exit - let window_ref = state.windows.remove(&event.window)?; - - state.loop_handle.remove(window_ref.refresh_event_token); - window_ref.window.destroy(); - - if state.windows.is_empty() { - state.common.signal.stop(); + // window "x" button clicked by user + if window.should_close() { + let window_ref = state.windows.remove(&event.window)?; + state.loop_handle.remove(window_ref.refresh_event_token); + // Rest of the close logic is handled in drop_window() } } } @@ -424,6 +440,8 @@ impl LinuxClient for X11Client { let x_window = state.xcb_connection.generate_id().unwrap(); let window = X11Window::new( + X11ClientStatePtr(Rc::downgrade(&self.0)), + state.common.foreground_executor.clone(), params, &state.xcb_connection, state.x_root_index, @@ -492,7 +510,7 @@ impl LinuxClient for X11Client { .expect("Failed to initialize refresh timer"); let window_ref = WindowRef { - window: window.clone(), + window: window.0.clone(), refresh_event_token, }; diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index a1d8532f71..5bbedffe17 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -2,10 +2,10 @@ #![allow(unused)] use crate::{ - platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas, - PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, - Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, - X11Client, X11ClientState, + platform::blade::BladeRenderer, size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, + Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, + PlatformWindow, Point, PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, + WindowOptions, WindowParams, X11Client, X11ClientState, X11ClientStatePtr, }; use blade_graphics as gpu; use parking_lot::Mutex; @@ -77,6 +77,8 @@ pub struct Callbacks { } pub(crate) struct X11WindowState { + client: X11ClientStatePtr, + executor: ForegroundExecutor, atoms: XcbAtoms, raw: RawWindow, bounds: Bounds, @@ -88,7 +90,7 @@ pub(crate) struct X11WindowState { } #[derive(Clone)] -pub(crate) struct X11Window { +pub(crate) struct X11WindowStatePtr { pub(crate) state: Rc>, pub(crate) callbacks: Rc>, xcb_connection: Rc, @@ -124,6 +126,8 @@ impl rwh::HasDisplayHandle for X11Window { impl X11WindowState { pub fn new( + client: X11ClientStatePtr, + executor: ForegroundExecutor, params: WindowParams, xcb_connection: &Rc, x_main_screen_index: usize, @@ -224,6 +228,8 @@ impl X11WindowState { let gpu_extent = query_render_extent(xcb_connection, x_window); Self { + client, + executor, display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()), raw, bounds: params.bounds.map(|v| v.0), @@ -244,16 +250,47 @@ impl X11WindowState { } } +pub(crate) struct X11Window(pub X11WindowStatePtr); + +impl Drop for X11Window { + fn drop(&mut self) { + let mut state = self.0.state.borrow_mut(); + state.renderer.destroy(); + + self.0.xcb_connection.unmap_window(self.0.x_window).unwrap(); + self.0 + .xcb_connection + .destroy_window(self.0.x_window) + .unwrap(); + self.0.xcb_connection.flush().unwrap(); + + let this_ptr = self.0.clone(); + let client_ptr = state.client.clone(); + state + .executor + .spawn(async move { + this_ptr.close(); + client_ptr.drop_window(this_ptr.x_window); + }) + .detach(); + drop(state); + } +} + impl X11Window { pub fn new( + client: X11ClientStatePtr, + executor: ForegroundExecutor, params: WindowParams, xcb_connection: &Rc, x_main_screen_index: usize, x_window: xproto::Window, atoms: &XcbAtoms, ) -> Self { - X11Window { + Self(X11WindowStatePtr { state: Rc::new(RefCell::new(X11WindowState::new( + client, + executor, params, xcb_connection, x_main_screen_index, @@ -263,20 +300,27 @@ impl X11Window { callbacks: Rc::new(RefCell::new(Callbacks::default())), xcb_connection: xcb_connection.clone(), x_window, + }) + } +} + +impl X11WindowStatePtr { + pub fn should_close(&self) -> bool { + let mut cb = self.callbacks.borrow_mut(); + if let Some(mut should_close) = cb.should_close.take() { + let result = (should_close)(); + cb.should_close = Some(should_close); + result + } else { + true } } - pub fn destroy(&self) { - let mut state = self.state.borrow_mut(); - state.renderer.destroy(); - drop(state); - - self.xcb_connection.unmap_window(self.x_window).unwrap(); - self.xcb_connection.destroy_window(self.x_window).unwrap(); - if let Some(fun) = self.callbacks.borrow_mut().close.take() { - fun(); + pub fn close(&self) { + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(fun) = callbacks.close.take() { + fun() } - self.xcb_connection.flush().unwrap(); } pub fn refresh(&self) { @@ -345,7 +389,7 @@ impl X11Window { impl PlatformWindow for X11Window { fn bounds(&self) -> Bounds { - self.state.borrow_mut().bounds.map(|v| v.into()) + self.0.state.borrow_mut().bounds.map(|v| v.into()) } // todo(linux) @@ -359,11 +403,11 @@ impl PlatformWindow for X11Window { } fn content_size(&self) -> Size { - self.state.borrow_mut().content_size() + self.0.state.borrow_mut().content_size() } fn scale_factor(&self) -> f32 { - self.state.borrow_mut().scale_factor + self.0.state.borrow_mut().scale_factor } // todo(linux) @@ -372,13 +416,14 @@ impl PlatformWindow for X11Window { } fn display(&self) -> Rc { - self.state.borrow().display.clone() + self.0.state.borrow().display.clone() } fn mouse_position(&self) -> Point { let reply = self + .0 .xcb_connection - .query_pointer(self.x_window) + .query_pointer(self.0.x_window) .unwrap() .reply() .unwrap(); @@ -395,11 +440,11 @@ impl PlatformWindow for X11Window { } fn set_input_handler(&mut self, input_handler: PlatformInputHandler) { - self.state.borrow_mut().input_handler = Some(input_handler); + self.0.state.borrow_mut().input_handler = Some(input_handler); } fn take_input_handler(&mut self) -> Option { - self.state.borrow_mut().input_handler.take() + self.0.state.borrow_mut().input_handler.take() } fn prompt( @@ -414,8 +459,9 @@ impl PlatformWindow for X11Window { fn activate(&self) { let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE); - self.xcb_connection - .configure_window(self.x_window, &win_aux) + self.0 + .xcb_connection + .configure_window(self.0.x_window, &win_aux) .log_err(); } @@ -425,22 +471,24 @@ impl PlatformWindow for X11Window { } fn set_title(&mut self, title: &str) { - self.xcb_connection + self.0 + .xcb_connection .change_property8( xproto::PropMode::REPLACE, - self.x_window, + self.0.x_window, xproto::AtomEnum::WM_NAME, xproto::AtomEnum::STRING, title.as_bytes(), ) .unwrap(); - self.xcb_connection + self.0 + .xcb_connection .change_property8( xproto::PropMode::REPLACE, - self.x_window, - self.state.borrow().atoms._NET_WM_NAME, - self.state.borrow().atoms.UTF8_STRING, + self.0.x_window, + self.0.state.borrow().atoms._NET_WM_NAME, + self.0.state.borrow().atoms.UTF8_STRING, title.as_bytes(), ) .unwrap(); @@ -484,39 +532,39 @@ impl PlatformWindow for X11Window { } fn on_request_frame(&self, callback: Box) { - self.callbacks.borrow_mut().request_frame = Some(callback); + self.0.callbacks.borrow_mut().request_frame = Some(callback); } fn on_input(&self, callback: Box crate::DispatchEventResult>) { - self.callbacks.borrow_mut().input = Some(callback); + self.0.callbacks.borrow_mut().input = Some(callback); } fn on_active_status_change(&self, callback: Box) { - self.callbacks.borrow_mut().active_status_change = Some(callback); + self.0.callbacks.borrow_mut().active_status_change = Some(callback); } fn on_resize(&self, callback: Box, f32)>) { - self.callbacks.borrow_mut().resize = Some(callback); + self.0.callbacks.borrow_mut().resize = Some(callback); } fn on_fullscreen(&self, callback: Box) { - self.callbacks.borrow_mut().fullscreen = Some(callback); + self.0.callbacks.borrow_mut().fullscreen = Some(callback); } fn on_moved(&self, callback: Box) { - self.callbacks.borrow_mut().moved = Some(callback); + self.0.callbacks.borrow_mut().moved = Some(callback); } fn on_should_close(&self, callback: Box bool>) { - self.callbacks.borrow_mut().should_close = Some(callback); + self.0.callbacks.borrow_mut().should_close = Some(callback); } fn on_close(&self, callback: Box) { - self.callbacks.borrow_mut().close = Some(callback); + self.0.callbacks.borrow_mut().close = Some(callback); } fn on_appearance_changed(&self, callback: Box) { - self.callbacks.borrow_mut().appearance_changed = Some(callback); + self.0.callbacks.borrow_mut().appearance_changed = Some(callback); } // todo(linux) @@ -525,12 +573,12 @@ impl PlatformWindow for X11Window { } fn draw(&self, scene: &Scene) { - let mut inner = self.state.borrow_mut(); + let mut inner = self.0.state.borrow_mut(); inner.renderer.draw(scene); } fn sprite_atlas(&self) -> sync::Arc { - let inner = self.state.borrow(); + let inner = self.0.state.borrow(); inner.renderer.sprite_atlas().clone() } } From 6a915e349c9098f9bd627999eaee48ccb3f8edbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sat, 27 Apr 2024 04:55:41 +0800 Subject: [PATCH 097/101] windows: Fix panicking on startup (#11028) ### Connection: Closes #10954 Release Notes: - N/A --- crates/gpui/src/platform/windows/window.rs | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index e05904426a..c76a7879a4 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -25,7 +25,7 @@ use windows::{ Win32::{ Foundation::*, Graphics::Gdi::*, - System::{Com::*, Ole::*, SystemServices::*}, + System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*}, UI::{ Controls::*, HiDpi::*, @@ -82,7 +82,9 @@ impl WindowsWindowInner { fn window_handle(&self) -> Result, rwh::HandleError> { Ok(unsafe { let hwnd = NonZeroIsize::new_unchecked(self.hwnd); - let handle = rwh::Win32WindowHandle::new(hwnd); + let mut handle = rwh::Win32WindowHandle::new(hwnd); + let hinstance = get_window_long(HWND(self.hwnd), GWLP_HINSTANCE); + handle.hinstance = NonZeroIsize::new(hinstance); rwh::WindowHandle::borrow_raw(handle.into()) }) } @@ -1269,7 +1271,7 @@ impl WindowsWindow { let nheight = options.bounds.size.height.0; let hwndparent = HWND::default(); let hmenu = HMENU::default(); - let hinstance = HINSTANCE::default(); + let hinstance = get_module_handle(); let mut context = WindowCreateContext { inner: None, platform_inner: platform_inner.clone(), @@ -1767,6 +1769,7 @@ fn register_wnd_class(icon_handle: HICON) -> PCWSTR { hIcon: icon_handle, lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), style: CS_HREDRAW | CS_VREDRAW, + hInstance: get_module_handle().into(), ..Default::default() }; unsafe { RegisterClassW(&wc) }; @@ -1907,6 +1910,20 @@ struct StyleAndBounds { cy: i32, } +fn get_module_handle() -> HMODULE { + unsafe { + let mut h_module = std::mem::zeroed(); + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + windows::core::w!("ZedModule"), + &mut h_module, + ) + .expect("Unable to get module handle"); // this should never fail + + h_module + } +} + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF; // https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN From 268cb948a7f0e7f7605b77909d4f5b0f469b1270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sat, 27 Apr 2024 04:56:48 +0800 Subject: [PATCH 098/101] windows: Move manifest file to `gpui` (#11036) This is a follow up of #10810 , `embed-resource` crate uses a different method to link the manifest file, so this makes moving manifest file to `gpui` possible. Now, examples can run as expected: ![Screenshot 2024-04-26 111559](https://github.com/zed-industries/zed/assets/14981363/bb040690-8129-490b-83b3-0a7d3cbd4953) TODO: - [ ] check if it builds with gnu toolchain Release Notes: - N/A --- Cargo.lock | 52 +++++++++++++++++-- crates/gpui/Cargo.toml | 3 ++ crates/gpui/build.rs | 9 ++++ .../resources/windows/gpui.manifest.xml} | 0 crates/gpui/resources/windows/gpui.rc | 2 + crates/storybook/Cargo.toml | 3 -- crates/storybook/build.rs | 11 ---- crates/zed/build.rs | 3 -- 8 files changed, 62 insertions(+), 21 deletions(-) rename crates/{zed/resources/windows/manifest.xml => gpui/resources/windows/gpui.manifest.xml} (100%) create mode 100644 crates/gpui/resources/windows/gpui.rc diff --git a/Cargo.lock b/Cargo.lock index d358f28f84..52c2558ed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3438,6 +3438,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embed-resource" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.10", + "vswhom", + "winreg 0.52.0", +] + [[package]] name = "emojis" version = "0.6.1" @@ -4536,6 +4550,7 @@ dependencies = [ "cosmic-text", "ctor", "derive_more", + "embed-resource", "env_logger", "etagere", "filedescriptor", @@ -5959,9 +5974,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memfd" @@ -7970,7 +7985,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -9499,7 +9514,6 @@ dependencies = [ "strum", "theme", "ui", - "winresource", ] [[package]] @@ -11133,6 +11147,26 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "vte" version = "0.13.0" @@ -12206,6 +12240,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "winresource" version = "0.1.17" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9198c99b7e..eb259dd41c 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -120,6 +120,9 @@ xkbcommon = { version = "0.7", features = ["wayland", "x11"] } [target.'cfg(windows)'.dependencies] windows.workspace = true +[target.'cfg(windows)'.build-dependencies] +embed-resource = "2.4" + [[example]] name = "hello_world" path = "examples/hello_world.rs" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 1c87b391f6..f9f38b626e 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -6,6 +6,15 @@ fn main() { #[cfg(target_os = "macos")] macos::build(); + + #[cfg(target_os = "windows")] + { + let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml"); + 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); + } } #[cfg(target_os = "macos")] diff --git a/crates/zed/resources/windows/manifest.xml b/crates/gpui/resources/windows/gpui.manifest.xml similarity index 100% rename from crates/zed/resources/windows/manifest.xml rename to crates/gpui/resources/windows/gpui.manifest.xml diff --git a/crates/gpui/resources/windows/gpui.rc b/crates/gpui/resources/windows/gpui.rc new file mode 100644 index 0000000000..a6f37877e8 --- /dev/null +++ b/crates/gpui/resources/windows/gpui.rc @@ -0,0 +1,2 @@ +#define RT_MANIFEST 24 +1 RT_MANIFEST "resources/windows/gpui.manifest.xml" \ No newline at end of file diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index b7fae8ebbc..1d7a828dd7 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -35,8 +35,5 @@ strum = { version = "0.25.0", features = ["derive"] } theme.workspace = true ui = { workspace = true, features = ["stories"] } -[target.'cfg(target_os = "windows")'.build-dependencies] -winresource = "0.1" - [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/build.rs b/crates/storybook/build.rs index d165aee5d5..4975cd33b7 100644 --- a/crates/storybook/build.rs +++ b/crates/storybook/build.rs @@ -9,16 +9,5 @@ fn main() { { println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); } - - let manifest = std::path::Path::new("../zed/resources/windows/manifest.xml"); - println!("cargo:rerun-if-changed={}", manifest.display()); - - let mut res = winresource::WindowsResource::new(); - res.set_manifest_file(manifest.to_str().unwrap()); - - if let Err(e) = res.compile() { - eprintln!("{}", e); - std::process::exit(1); - } } } diff --git a/crates/zed/build.rs b/crates/zed/build.rs index a1126afed7..4a8f2c81fd 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -52,14 +52,11 @@ fn main() { println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); } - let manifest = std::path::Path::new("resources/windows/manifest.xml"); let icon = std::path::Path::new("resources/windows/app-icon.ico"); - println!("cargo:rerun-if-changed={}", manifest.display()); println!("cargo:rerun-if-changed={}", icon.display()); let mut res = winresource::WindowsResource::new(); res.set_icon(icon.to_str().unwrap()); - res.set_manifest_file(manifest.to_str().unwrap()); if let Err(e) = res.compile() { eprintln!("{}", e); From 11dc3c2582f1e61a416e3a7abf658ef05a4472d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Sat, 27 Apr 2024 04:58:12 +0800 Subject: [PATCH 099/101] windows: Support all `OpenType` font features (#10756) Release Notes: - Added support for all `OpenType` font features to DirectWrite. https://github.com/zed-industries/zed/assets/14981363/cb2848cd-9178-4d87-881a-54dc646b2b61 --------- Co-authored-by: Mikayla Maki --- crates/assistant/src/assistant_panel.rs | 2 +- .../src/completion_provider/open_ai.rs | 2 +- .../src/chat_panel/message_editor.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/editor/src/editor.rs | 6 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- .../src/platform/cosmic_text/text_system.rs | 4 +- crates/gpui/src/platform/mac/open_type.rs | 2 +- crates/gpui/src/platform/mac/text_system.rs | 10 ++- crates/gpui/src/style.rs | 2 +- crates/gpui/src/text_system/font_features.rs | 86 ++++++++++++++++++- crates/outline/src/outline.rs | 2 +- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 3 +- crates/theme/src/settings.rs | 8 +- crates/ui_text_field/src/ui_text_field.rs | 2 +- 17 files changed, 114 insertions(+), 25 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index cfb8b983bb..499feae388 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2873,7 +2873,7 @@ impl InlineAssistant { cx.theme().colors().text }, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/assistant/src/completion_provider/open_ai.rs b/crates/assistant/src/completion_provider/open_ai.rs index 458a3a9d25..9a7398ef7f 100644 --- a/crates/assistant/src/completion_provider/open_ai.rs +++ b/crates/assistant/src/completion_provider/open_ai.rs @@ -241,7 +241,7 @@ impl AuthenticationPrompt { let text_style = TextStyle { color: cx.theme().colors().text, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index f00575c8fd..84014f05c6 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -522,7 +522,7 @@ impl Render for MessageEditor { cx.theme().colors().text }, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: TextSize::Small.rems(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 7f843b7a86..4b19c4fb25 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2171,7 +2171,7 @@ impl CollabPanel { cx.theme().colors().text }, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2b299e780..cb751a1169 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10340,7 +10340,7 @@ impl Render for Editor { EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, @@ -10353,7 +10353,7 @@ impl Render for Editor { EditorMode::Full => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, + font_features: settings.buffer_font.features.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, @@ -10778,7 +10778,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren let theme_settings = ThemeSettings::get_global(cx); text_style.font_family = theme_settings.buffer_font.family.clone(); text_style.font_style = theme_settings.buffer_font.style; - text_style.font_features = theme_settings.buffer_font.features; + text_style.font_features = theme_settings.buffer_font.features.clone(); text_style.font_weight = theme_settings.buffer_font.weight; let multi_line_diagnostic = diagnostic.message.contains('\n'); diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fcc1fd8695..34d9a2ca4d 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -739,7 +739,7 @@ impl ExtensionsPage { cx.theme().colors().text }, font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, + font_features: settings.ui_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/gpui/src/platform/cosmic_text/text_system.rs b/crates/gpui/src/platform/cosmic_text/text_system.rs index 13467361e4..a6f131ff7f 100644 --- a/crates/gpui/src/platform/cosmic_text/text_system.rs +++ b/crates/gpui/src/platform/cosmic_text/text_system.rs @@ -90,7 +90,7 @@ impl PlatformTextSystem for CosmicTextSystem { let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) { font_ids.as_slice() } else { - let font_ids = state.load_family(&font.family, font.features)?; + let font_ids = state.load_family(&font.family, &font.features)?; state .font_ids_by_family_cache .insert(font.family.clone(), font_ids); @@ -211,7 +211,7 @@ impl CosmicTextSystemState { fn load_family( &mut self, name: &str, - _features: FontFeatures, + _features: &FontFeatures, ) -> Result> { // TODO: Determine the proper system UI font. let name = if name == ".SystemUIFont" { diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index c9d7197c0d..d465e8f745 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -107,7 +107,7 @@ const kTypographicExtrasType: i32 = 14; const kVerticalFractionsSelector: i32 = 1; const kVerticalPositionType: i32 = 10; -pub fn apply_features(font: &mut Font, features: FontFeatures) { +pub fn apply_features(font: &mut Font, features: &FontFeatures) { // See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc // for a reference implementation. toggle_open_type_feature( diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 64ba6cbd36..6c4a10af00 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -123,12 +123,12 @@ impl PlatformTextSystem for MacTextSystem { let mut lock = RwLockUpgradableReadGuard::upgrade(lock); let font_key = FontKey { font_family: font.family.clone(), - font_features: font.features, + font_features: font.features.clone(), }; let candidates = if let Some(font_ids) = lock.font_ids_by_font_key.get(&font_key) { font_ids.as_slice() } else { - let font_ids = lock.load_family(&font.family, font.features)?; + let font_ids = lock.load_family(&font.family, &font.features)?; lock.font_ids_by_font_key.insert(font_key.clone(), font_ids); lock.font_ids_by_font_key[&font_key].as_ref() }; @@ -219,7 +219,11 @@ impl MacTextSystemState { Ok(()) } - fn load_family(&mut self, name: &str, features: FontFeatures) -> Result> { + fn load_family( + &mut self, + name: &str, + features: &FontFeatures, + ) -> Result> { let name = if name == ".SystemUIFont" { ".AppleSystemUIFont" } else { diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 6d7e3ac94e..49111f48f8 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -262,7 +262,7 @@ impl TextStyle { pub fn font(&self) -> Font { Font { family: self.font_family.clone(), - features: self.font_features, + features: self.font_features.clone(), weight: self.font_weight, style: self.font_style, } diff --git a/crates/gpui/src/text_system/font_features.rs b/crates/gpui/src/text_system/font_features.rs index 303cdbef26..39ec18f74b 100644 --- a/crates/gpui/src/text_system/font_features.rs +++ b/crates/gpui/src/text_system/font_features.rs @@ -1,3 +1,7 @@ +#[cfg(target_os = "windows")] +use crate::SharedString; +#[cfg(target_os = "windows")] +use itertools::Itertools; use schemars::{ schema::{InstanceType, Schema, SchemaObject, SingleOrVec}, JsonSchema, @@ -7,10 +11,14 @@ macro_rules! create_definitions { ($($(#[$meta:meta])* ($name:ident, $idx:expr)),* $(,)?) => { /// The OpenType features that can be configured for a given font. - #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] + #[derive(Default, Clone, Eq, PartialEq, Hash)] pub struct FontFeatures { enabled: u64, disabled: u64, + #[cfg(target_os = "windows")] + other_enabled: SharedString, + #[cfg(target_os = "windows")] + other_disabled: SharedString, } impl FontFeatures { @@ -47,6 +55,14 @@ macro_rules! create_definitions { } } )* + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + result.push((name.collect::(), true)); + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + result.push((name.collect::(), false)); + } + } result } } @@ -59,6 +75,15 @@ macro_rules! create_definitions { debug.field(stringify!($name), &value); }; )* + #[cfg(target_os = "windows")] + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + debug.field(name.collect::().as_str(), &true); + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + debug.field(name.collect::().as_str(), &false); + } + } debug.finish() } } @@ -80,6 +105,7 @@ macro_rules! create_definitions { formatter.write_str("a map of font features") } + #[cfg(not(target_os = "windows"))] fn visit_map(self, mut access: M) -> Result where M: MapAccess<'de>, @@ -100,6 +126,54 @@ macro_rules! create_definitions { } Ok(FontFeatures { enabled, disabled }) } + + #[cfg(target_os = "windows")] + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut enabled: u64 = 0; + let mut disabled: u64 = 0; + let mut other_enabled = "".to_owned(); + let mut other_disabled = "".to_owned(); + + while let Some((key, value)) = access.next_entry::>()? { + let idx = match key.as_str() { + $(stringify!($name) => Some($idx),)* + other_feature => { + if other_feature.len() != 4 || !other_feature.is_ascii() { + log::error!("Incorrect feature name: {}", other_feature); + continue; + } + None + }, + }; + if let Some(idx) = idx { + match value { + Some(true) => enabled |= 1 << idx, + Some(false) => disabled |= 1 << idx, + None => {} + }; + } else { + match value { + Some(true) => other_enabled.push_str(key.as_str()), + Some(false) => other_disabled.push_str(key.as_str()), + None => {} + }; + } + } + let other_enabled = if other_enabled.is_empty() { + "".into() + } else { + other_enabled.into() + }; + let other_disabled = if other_disabled.is_empty() { + "".into() + } else { + other_disabled.into() + }; + Ok(FontFeatures { enabled, disabled, other_enabled, other_disabled }) + } } let features = deserializer.deserialize_map(FontFeaturesVisitor)?; @@ -125,6 +199,16 @@ macro_rules! create_definitions { } )* + #[cfg(target_os = "windows")] + { + for name in self.other_enabled.as_ref().chars().chunks(4).into_iter() { + map.serialize_entry(name.collect::().as_str(), &true)?; + } + for name in self.other_disabled.as_ref().chars().chunks(4).into_iter() { + map.serialize_entry(name.collect::().as_str(), &false)?; + } + } + map.end() } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 63a49e6220..dab806ad54 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -274,7 +274,7 @@ impl PickerDelegate for OutlineViewDelegate { let text_style = TextStyle { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, + font_features: settings.buffer_font.features.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 0e0c33c265..3e0b8fb748 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -114,7 +114,7 @@ impl BufferSearchBar { color }, font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, + font_features: settings.buffer_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 111a0ace61..c75e5ac6a6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1307,7 +1307,7 @@ impl ProjectSearchBar { cx.theme().colors().text }, font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, + font_features: settings.buffer_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 909ab2a6f1..b0bee7fa5e 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -578,7 +578,8 @@ impl Element for TerminalElement { let font_features = terminal_settings .font_features - .unwrap_or(settings.buffer_font.features); + .clone() + .unwrap_or(settings.buffer_font.features.clone()); let line_height = terminal_settings.line_height.value(); let font_size = terminal_settings.font_size; diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index b7f08b2450..e5a6b6b2c7 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -325,13 +325,13 @@ impl settings::Settings for ThemeSettings { ui_font_size: defaults.ui_font_size.unwrap().into(), ui_font: Font { family: defaults.ui_font_family.clone().unwrap().into(), - features: defaults.ui_font_features.unwrap(), + features: defaults.ui_font_features.clone().unwrap(), weight: Default::default(), style: Default::default(), }, buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), - features: defaults.buffer_font_features.unwrap(), + features: defaults.buffer_font_features.clone().unwrap(), weight: FontWeight::default(), style: FontStyle::default(), }, @@ -349,14 +349,14 @@ impl settings::Settings for ThemeSettings { if let Some(value) = value.buffer_font_family.clone() { this.buffer_font.family = value.into(); } - if let Some(value) = value.buffer_font_features { + if let Some(value) = value.buffer_font_features.clone() { this.buffer_font.features = value; } if let Some(value) = value.ui_font_family.clone() { this.ui_font.family = value.into(); } - if let Some(value) = value.ui_font_features { + if let Some(value) = value.ui_font_features.clone() { this.ui_font.features = value; } diff --git a/crates/ui_text_field/src/ui_text_field.rs b/crates/ui_text_field/src/ui_text_field.rs index f5addf3a9f..548756de1f 100644 --- a/crates/ui_text_field/src/ui_text_field.rs +++ b/crates/ui_text_field/src/ui_text_field.rs @@ -123,7 +123,7 @@ impl Render for TextField { let text_style = TextStyle { font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, + font_features: settings.buffer_font.features.clone(), font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, From 7bd18fa6539abf1c369692f8cbe8d6501d00b50a Mon Sep 17 00:00:00 2001 From: Akilan Elango Date: Sat, 27 Apr 2024 02:33:19 +0530 Subject: [PATCH 100/101] Sync maximized state from top-level configure event for a wayland window (#11003) * Otherwise is_maximized always returns `true` Release Notes: - Fixed maximized state. Tested with a dummy maximize/restore button with the `zoom()` (not implemented yet). Without the right `maximized`, in toggle zoom function is not possible to call `set_maximized()` or `unset_maximized()`. ```rust fn zoom(&self) { if self.is_maximized() { self.borrow_mut().toplevel.unset_maximized(); } else { self.borrow_mut().toplevel.set_maximized(); } } ``` --- crates/gpui/src/platform/linux/wayland/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 2b1b8d1c7f..82bfa6c0a8 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -322,7 +322,7 @@ impl WaylandWindowStatePtr { self.resize(width, height); self.set_fullscreen(fullscreen); let mut state = self.state.borrow_mut(); - state.maximized = true; + state.maximized = maximized; false } From 393b16d2262d6bf94ea0ea9c982ab2b1c2736fae Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Fri, 26 Apr 2024 23:07:05 +0200 Subject: [PATCH 101/101] Fix Wayland keyrepeat getting cancelled by unrelated keyup (#11052) fixes #11048 ## Problem in the situation `press right`, `press left`, `release right` the following happens right now: - `keypressed right`, `current_keysym` is set to `right` - `keypressed left`, `current_keysym` is set to `left` the repeat timer runs asynchronously and emits keyrepeats since `current_keysym.is_some()` - `keyreleased right`, `current_keysym` is set to None the repeat timer no longer emits keyrepeats - `keyreleased left`, this is where `current_keysym` should actually be set to None. ## Solution Only reset `current_keysym` if the released key matches the last pressed key. Release Notes: - N/A --- crates/gpui/src/platform/linux/wayland/client.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 8f23b22921..698134bcbc 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -843,7 +843,9 @@ impl Dispatch for WaylandClientStatePtr { keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), }); - state.repeat.current_keysym = None; + if state.repeat.current_keysym == Some(keysym) { + state.repeat.current_keysym = None; + } drop(state); focused_window.handle_input(input);