From 28c5e33e0c114d0813ae275508edca7113cbaa5f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:17:20 +0200 Subject: [PATCH 01/40] JSON: Fix validation being disabled following #13459 (#13770) The problem with #13459 was the bump to a newer JSON LS version, which requires explicitly opting into validation. Release Notes: - Fixed JSON validation being disabled by default (Preview only) --- crates/languages/src/json.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ac869232b6c213..11e67c15ce9c0b 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -95,6 +95,10 @@ impl JsonLspAdapter { "format": { "enable": true, }, + "validate": + { + "enable": true, + }, "schemas": [ { "fileMatch": ["tsconfig.json"], From 351a3c0815a5aab8c4e1e624eae2caa73368afc0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 3 Jul 2024 08:05:40 -0400 Subject: [PATCH 02/40] docs: Improve default settings comments (#13749) - Add the phrase "compact folders" to `auto_fold_dirs` to enhance searchability. - Fix `buffer_line_height` copy pasta Release Notes: - N/A --- assets/settings/default.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 21768fb0fa83dd..c07bc601bf69eb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -38,11 +38,11 @@ // Set the buffer's line height. // May take 3 values: // 1. Use a line height that's comfortable for reading (1.618) - // "line_height": "comfortable" + // "buffer_line_height": "comfortable" // 2. Use a standard line height, (1.3) - // "line_height": "standard", + // "buffer_line_height": "standard", // 3. Use a custom line height - // "line_height": { + // "buffer_line_height": { // "custom": 2 // }, "buffer_line_height": "comfortable", @@ -307,8 +307,8 @@ // when a corresponding project entry becomes active. // Gitignored entries are never auto revealed. "auto_reveal_entries": true, - /// Whether to fold directories automatically - /// when a directory has only one directory inside. + // Whether to fold directories automatically and show compact folders + // (e.g. "a/b/c" ) when a directory has only one subdirectory inside. "auto_fold_dirs": false, /// Scrollbar-related settings "scrollbar": { From c1e18059f891d34ade1db2de10e3a41fa64ce9c4 Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 3 Jul 2024 14:12:24 +0100 Subject: [PATCH 03/40] gpui: Prefer integrated GPUs on Intel Mac (#13685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Intel, Metal will pick a discrete GPU by default when available, resulting in higher power consumption and heat output. Prefer non‐removable low‐power devices to correct this. On Apple Silicon, there is only ever one GPU, so there is no functional change. I didn’t do intensive benchmarking of this or anything, but Zed still seems responsive and it stops my MacBook Pro acting as a combination space heater–jet engine. Thanks to @denlukia for showing that this is easy to fix; I’ve marked you as a co‐author, I hope that’s okay. Closes: #5124 Release Notes: - Improved power consumption on Intel Macs by preferring integrated GPUs over the discrete GPUs. ([#5124](https://github.com/zed-industries/zed/issues/5124)). Co-authored-by: Denis Lukianenko --- crates/gpui/src/platform/mac/metal_renderer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index f6818fe3de0b0c..97d788412febcc 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -109,9 +109,12 @@ pub(crate) struct MetalRenderer { impl MetalRenderer { pub fn new(instance_buffer_pool: Arc>) -> Self { - let device: metal::Device = if let Some(device) = metal::Device::system_default() { - device - } else { + // Prefer low‐power integrated GPUs on Intel Mac. On Apple + // Silicon, there is only ever one GPU, so this is equivalent to + // `metal::Device::system_default()`. + let mut devices = metal::Device::all(); + devices.sort_by_key(|device| (!device.is_removable(), device.is_low_power())); + let Some(device) = devices.pop() else { log::error!("unable to access a compatible graphics device"); std::process::exit(1); }; From dceb0827e880f13d77c5b34f29bb8e5678e98ff3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 10:14:57 -0400 Subject: [PATCH 04/40] Rename `ExtensionDocsIndexer` to `ExtensionIndexedDocsProvider` (#13776) This PR renames `ExtensionDocsIndexer` to `ExtensionIndexedDocsProvider` to better align with the name of the trait it implements. Release Notes: - N/A --- ...n_docs_indexer.rs => extension_indexed_docs_provider.rs} | 4 ++-- crates/extension/src/extension_store.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename crates/extension/src/{extension_docs_indexer.rs => extension_indexed_docs_provider.rs} (94%) diff --git a/crates/extension/src/extension_docs_indexer.rs b/crates/extension/src/extension_indexed_docs_provider.rs similarity index 94% rename from crates/extension/src/extension_docs_indexer.rs rename to crates/extension/src/extension_indexed_docs_provider.rs index 7ae276800766c6..8efb29f98def37 100644 --- a/crates/extension/src/extension_docs_indexer.rs +++ b/crates/extension/src/extension_indexed_docs_provider.rs @@ -9,14 +9,14 @@ use wasmtime_wasi::WasiView; use crate::wasm_host::{WasmExtension, WasmHost}; -pub struct ExtensionDocsIndexer { +pub struct ExtensionIndexedDocsProvider { pub(crate) extension: WasmExtension, pub(crate) host: Arc, pub(crate) id: ProviderId, } #[async_trait] -impl IndexedDocsProvider for ExtensionDocsIndexer { +impl IndexedDocsProvider for ExtensionIndexedDocsProvider { fn id(&self) -> ProviderId { self.id.clone() } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index d80f3ef3dc6e33..ccb679c2305930 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -1,5 +1,5 @@ pub mod extension_builder; -mod extension_docs_indexer; +mod extension_indexed_docs_provider; mod extension_lsp_adapter; mod extension_manifest; mod extension_settings; @@ -9,7 +9,7 @@ mod wasm_host; #[cfg(test)] mod extension_store_test; -use crate::extension_docs_indexer::ExtensionDocsIndexer; +use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; use crate::extension_manifest::SchemaVersion; use crate::extension_slash_command::ExtensionSlashCommand; use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit}; @@ -1202,7 +1202,7 @@ impl ExtensionStore { for (provider_id, _provider) in &manifest.indexed_docs_providers { this.indexed_docs_registry.register_provider(Box::new( - ExtensionDocsIndexer { + ExtensionIndexedDocsProvider { extension: wasm_extension.clone(), host: this.wasm_host.clone(), id: ProviderId(provider_id.clone()), From 3348c3ab4c5b84986acb39375ffaffd453a83d5e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jul 2024 09:03:39 -0600 Subject: [PATCH 05/40] vim: Support for q and @ (#13761) Fixes: #1504 Release Notes: - vim: Support for macros (`q` and `@`) to record and replay (#1506, #4448) --- assets/keymaps/vim.json | 278 ++++----------- crates/vim/src/insert.rs | 2 +- crates/vim/src/mode_indicator.rs | 7 +- crates/vim/src/normal/repeat.rs | 324 +++++++++++++++--- crates/vim/src/state.rs | 21 +- crates/vim/src/vim.rs | 71 ++-- crates/vim/src/visual.rs | 2 +- crates/vim/test_data/test_record_replay.json | 14 + .../test_data/test_record_replay_count.json | 16 + .../vim/test_data/test_record_replay_dot.json | 17 + .../test_record_replay_interleaved.json | 35 ++ .../test_data/test_record_replay_of_dot.json | 14 + docs/src/vim.md | 4 +- 13 files changed, 490 insertions(+), 315 deletions(-) create mode 100644 crates/vim/test_data/test_record_replay.json create mode 100644 crates/vim/test_data/test_record_replay_count.json create mode 100644 crates/vim/test_data/test_record_replay_dot.json create mode 100644 crates/vim/test_data/test_record_replay_interleaved.json create mode 100644 crates/vim/test_data/test_record_replay_of_dot.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index cd431785222ace..498fd018fe59e5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -126,10 +126,7 @@ } } ], - "m": [ - "vim::PushOperator", - "Mark" - ], + "m": ["vim::PushOperator", "Mark"], "'": [ "vim::PushOperator", { @@ -151,14 +148,8 @@ "ctrl-o": "pane::GoBack", "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl-[": [ - "vim::SwitchMode", - "Normal" - ], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", @@ -284,10 +275,7 @@ // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", - "z .": [ - "workspace::SendKeystrokes", - "z z ^" - ], + "z .": ["workspace::SendKeystrokes", "z z ^"], "z b": "editor::ScrollCursorBottom", "z c": "editor::Fold", "z o": "editor::UnfoldLines", @@ -305,123 +293,36 @@ } ], // Count support - "1": [ - "vim::Number", - 1 - ], - "2": [ - "vim::Number", - 2 - ], - "3": [ - "vim::Number", - 3 - ], - "4": [ - "vim::Number", - 4 - ], - "5": [ - "vim::Number", - 5 - ], - "6": [ - "vim::Number", - 6 - ], - "7": [ - "vim::Number", - 7 - ], - "8": [ - "vim::Number", - 8 - ], - "9": [ - "vim::Number", - 9 - ], + "1": ["vim::Number", 1], + "2": ["vim::Number", 2], + "3": ["vim::Number", 3], + "4": ["vim::Number", 4], + "5": ["vim::Number", 5], + "6": ["vim::Number", 6], + "7": ["vim::Number", 7], + "8": ["vim::Number", 8], + "9": ["vim::Number", 9], // window related commands (ctrl-w X) - "ctrl-w left": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w right": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w up": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w down": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w ctrl-h": [ - "workspace::ActivatePaneInDirection", - "Left" - ], - "ctrl-w ctrl-l": [ - "workspace::ActivatePaneInDirection", - "Right" - ], - "ctrl-w ctrl-k": [ - "workspace::ActivatePaneInDirection", - "Up" - ], - "ctrl-w ctrl-j": [ - "workspace::ActivatePaneInDirection", - "Down" - ], - "ctrl-w shift-left": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-right": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-up": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-down": [ - "workspace::SwapPaneInDirection", - "Down" - ], - "ctrl-w shift-h": [ - "workspace::SwapPaneInDirection", - "Left" - ], - "ctrl-w shift-l": [ - "workspace::SwapPaneInDirection", - "Right" - ], - "ctrl-w shift-k": [ - "workspace::SwapPaneInDirection", - "Up" - ], - "ctrl-w shift-j": [ - "workspace::SwapPaneInDirection", - "Down" - ], + "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", @@ -443,14 +344,8 @@ "ctrl-w ctrl-q": "pane::CloseAllItems", "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": [ - "workspace::NewFileInDirection", - "Up" - ], - "ctrl-w ctrl-n": [ - "workspace::NewFileInDirection", - "Up" - ], + "ctrl-w n": ["workspace::NewFileInDirection", "Up"], + "ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"], "ctrl-w d": "editor::GoToDefinitionSplit", "ctrl-w g d": "editor::GoToDefinitionSplit", "ctrl-w shift-d": "editor::GoToTypeDefinitionSplit", @@ -472,21 +367,12 @@ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { ".": "vim::Repeat", - "c": [ - "vim::PushOperator", - "Change" - ], + "c": ["vim::PushOperator", "Change"], "shift-c": "vim::ChangeToEndOfLine", - "d": [ - "vim::PushOperator", - "Delete" - ], + "d": ["vim::PushOperator", "Delete"], "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", - "y": [ - "vim::PushOperator", - "Yank" - ], + "y": ["vim::PushOperator", "Yank"], "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", @@ -508,36 +394,18 @@ ], "u": "editor::Undo", "ctrl-r": "editor::Redo", - "r": [ - "vim::PushOperator", - "Replace" - ], + "r": ["vim::PushOperator", "Replace"], "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", - ">": [ - "vim::PushOperator", - "Indent" - ], - "<": [ - "vim::PushOperator", - "Outdent" - ], - "g u": [ - "vim::PushOperator", - "Lowercase" - ], - "g shift-u": [ - "vim::PushOperator", - "Uppercase" - ], - "g ~": [ - "vim::PushOperator", - "OppositeCase" - ], - "\"": [ - "vim::PushOperator", - "Register" - ], + ">": ["vim::PushOperator", "Indent"], + "<": ["vim::PushOperator", "Outdent"], + "g u": ["vim::PushOperator", "Lowercase"], + "g shift-u": ["vim::PushOperator", "Uppercase"], + "g ~": ["vim::PushOperator", "OppositeCase"], + "\"": ["vim::PushOperator", "Register"], + "q": "vim::ToggleRecord", + "shift-q": "vim::ReplayLastRecording", + "@": ["vim::PushOperator", "ReplayRegister"], "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-pageup": "pane::ActivatePrevItem", // tree-sitter related commands @@ -552,10 +420,7 @@ { "context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting", "bindings": { - "\"": [ - "vim::PushOperator", - "Register" - ], + "\"": ["vim::PushOperator", "Register"], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", "] x": "editor::SelectSmallerSyntaxNode" @@ -564,10 +429,7 @@ { "context": "Editor && VimCount && vim_mode != insert", "bindings": { - "0": [ - "vim::Number", - 0 - ] + "0": ["vim::Number", 0] } }, { @@ -618,10 +480,7 @@ { "context": "Editor && vim_mode == normal && vim_operator == d", "bindings": { - "s": [ - "vim::PushOperator", - "DeleteSurrounds" - ] + "s": ["vim::PushOperator", "DeleteSurrounds"] } }, { @@ -743,22 +602,10 @@ "shift-i": "vim::InsertBefore", "shift-a": "vim::InsertAfter", "shift-j": "vim::JoinLines", - "r": [ - "vim::PushOperator", - "Replace" - ], - "ctrl-c": [ - "vim::SwitchMode", - "Normal" - ], - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl-[": [ - "vim::SwitchMode", - "Normal" - ], + "r": ["vim::PushOperator", "Replace"], + "ctrl-c": ["vim::SwitchMode", "Normal"], + "escape": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"], ">": "vim::Indent", "<": "vim::Outdent", "i": [ @@ -806,10 +653,7 @@ "ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", - "ctrl-r": [ - "vim::PushOperator", - "Register" - ] + "ctrl-r": ["vim::PushOperator", "Register"] } }, { @@ -828,14 +672,8 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": [ - "vim::SwitchMode", - "Normal" - ], - "ctrl-[": [ - "vim::SwitchMode", - "Normal" - ] + "escape": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"] } }, { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index ad97a4af3d95a6..fbf2d5fc7761eb 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -23,7 +23,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext< } let count = vim.take_count(cx).unwrap_or(1); vim.stop_recording_immediately(action.boxed_clone()); - if count <= 1 || vim.workspace_state.replaying { + if count <= 1 || vim.workspace_state.dot_replaying { create_mark(vim, "^".into(), false, cx); vim.update_active_editor(cx, |_, editor, cx| { editor.dismiss_menus_and_popups(false, cx); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 32b38b2beab627..1cfb598e7f4ff0 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -61,10 +61,11 @@ impl ModeIndicator { } fn current_operators_description(&self, vim: &Vim) -> String { - vim.state() - .pre_count - .map(|count| format!("{}", count)) + vim.workspace_state + .recording_register + .map(|reg| format!("recording @{reg} ")) .into_iter() + .chain(vim.state().pre_count.map(|count| format!("{}", count))) .chain(vim.state().selected_register.map(|reg| format!("\"{reg}"))) .chain( vim.state() diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index b9f5055162590e..fd4365b006bded 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -1,14 +1,17 @@ +use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc}; + use crate::{ insert::NormalBefore, motion::Motion, - state::{Mode, RecordedSelection, ReplayableAction}, + state::{Mode, Operator, RecordedSelection, ReplayableAction}, visual::visual_motion, Vim, }; use gpui::{actions, Action, ViewContext, WindowContext}; +use util::ResultExt; use workspace::Workspace; -actions!(vim, [Repeat, EndRepeat]); +actions!(vim, [Repeat, EndRepeat, ToggleRecord, ReplayLastRecording]); fn should_replay(action: &Box) -> bool { // skip so that we don't leave the character palette open @@ -44,24 +47,148 @@ fn repeatable_insert(action: &ReplayableAction) -> Option> { pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|_: &mut Workspace, _: &EndRepeat, cx| { Vim::update(cx, |vim, cx| { - vim.workspace_state.replaying = false; + vim.workspace_state.dot_replaying = false; vim.switch_mode(Mode::Normal, false, cx) }); }); workspace.register_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false)); + workspace.register_action(|_: &mut Workspace, _: &ToggleRecord, cx| { + Vim::update(cx, |vim, cx| { + if let Some(char) = vim.workspace_state.recording_register.take() { + vim.workspace_state.last_recorded_register = Some(char) + } else { + vim.push_operator(Operator::RecordRegister, cx); + } + }) + }); + + workspace.register_action(|_: &mut Workspace, _: &ReplayLastRecording, cx| { + let Some(register) = Vim::read(cx).workspace_state.last_recorded_register else { + return; + }; + replay_register(register, cx) + }); +} + +pub struct ReplayerState { + actions: Vec, + running: bool, + ix: usize, +} + +#[derive(Clone)] +pub struct Replayer(Rc>); + +impl Replayer { + pub fn new() -> Self { + Self(Rc::new(RefCell::new(ReplayerState { + actions: vec![], + running: false, + ix: 0, + }))) + } + + pub fn replay(&mut self, actions: Vec, cx: &mut WindowContext) { + let mut lock = self.0.borrow_mut(); + let range = lock.ix..lock.ix; + lock.actions.splice(range, actions); + if lock.running { + return; + } + lock.running = true; + let this = self.clone(); + cx.defer(move |cx| this.next(cx)) + } + + pub fn stop(self) { + self.0.borrow_mut().actions.clear() + } + + pub fn next(self, cx: &mut WindowContext) { + let mut lock = self.0.borrow_mut(); + let action = if lock.ix < 10000 { + lock.actions.get(lock.ix).cloned() + } else { + log::error!("Aborting replay after 10000 actions"); + None + }; + lock.ix += 1; + drop(lock); + let Some(action) = action else { + Vim::update(cx, |vim, _| vim.workspace_state.replayer.take()); + return; + }; + match action { + ReplayableAction::Action(action) => { + if should_replay(&action) { + cx.dispatch_action(action.boxed_clone()); + cx.defer(move |cx| observe_action(action.boxed_clone(), cx)); + } + } + ReplayableAction::Insertion { + text, + utf16_range_to_replace, + } => { + if let Some(editor) = Vim::read(cx).active_editor.clone() { + editor + .update(cx, |editor, cx| { + editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) + }) + .log_err(); + } + } + } + cx.defer(move |cx| self.next(cx)); + } +} + +pub(crate) fn record_register(register: char, cx: &mut WindowContext) { + Vim::update(cx, |vim, cx| { + vim.workspace_state.recording_register = Some(register); + vim.workspace_state.recordings.remove(®ister); + vim.workspace_state.ignore_current_insertion = true; + vim.clear_operator(cx) + }) +} + +pub(crate) fn replay_register(mut register: char, cx: &mut WindowContext) { + Vim::update(cx, |vim, cx| { + let mut count = vim.take_count(cx).unwrap_or(1); + vim.clear_operator(cx); + + if register == '@' { + let Some(last) = vim.workspace_state.last_replayed_register else { + return; + }; + register = last; + } + let Some(actions) = vim.workspace_state.recordings.get(®ister) else { + return; + }; + + let mut repeated_actions = vec![]; + while count > 0 { + repeated_actions.extend(actions.iter().cloned()); + count -= 1 + } + + vim.workspace_state.last_replayed_register = Some(register); + + vim.workspace_state + .replayer + .get_or_insert_with(|| Replayer::new()) + .replay(repeated_actions, cx); + }); } pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { - let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| { + let Some((mut actions, selection)) = Vim::update(cx, |vim, cx| { let actions = vim.workspace_state.recorded_actions.clone(); if actions.is_empty() { return None; } - let Some(editor) = vim.active_editor.clone() else { - return None; - }; let count = vim.take_count(cx); let selection = vim.workspace_state.recorded_selection.clone(); @@ -85,7 +212,17 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { } } - Some((actions, editor, selection)) + if vim.workspace_state.replayer.is_none() { + if let Some(recording_register) = vim.workspace_state.recording_register { + vim.workspace_state + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(Repeat.boxed_clone())); + } + } + + Some((actions, selection)) }) else { return; }; @@ -167,42 +304,75 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { actions = new_actions; } - Vim::update(cx, |vim, _| vim.workspace_state.replaying = true); - let window = cx.window_handle(); - cx.spawn(move |mut cx| async move { - editor.update(&mut cx, |editor, _| { - editor.show_local_selections = false; - })?; - for action in actions { - if !matches!( - cx.update(|cx| Vim::read(cx).workspace_state.replaying), - Ok(true) - ) { - break; - } + actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - match action { - ReplayableAction::Action(action) => { - if should_replay(&action) { - window.update(&mut cx, |_, cx| cx.dispatch_action(action)) - } else { - Ok(()) - } - } - ReplayableAction::Insertion { - text, - utf16_range_to_replace, - } => editor.update(&mut cx, |editor, cx| { - editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx) - }), - }? + Vim::update(cx, |vim, cx| { + vim.workspace_state.dot_replaying = true; + + vim.workspace_state + .replayer + .get_or_insert_with(|| Replayer::new()) + .replay(actions, cx); + }) +} + +pub(crate) fn observe_action(action: Box, cx: &mut WindowContext) { + Vim::update(cx, |vim, _| { + if vim.workspace_state.dot_recording { + vim.workspace_state + .recorded_actions + .push(ReplayableAction::Action(action.boxed_clone())); + + if vim.workspace_state.stop_recording_after_next_action { + vim.workspace_state.dot_recording = false; + vim.workspace_state.stop_recording_after_next_action = false; + } + } + if vim.workspace_state.replayer.is_none() { + if let Some(recording_register) = vim.workspace_state.recording_register { + vim.workspace_state + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Action(action)); + } } - editor.update(&mut cx, |editor, _| { - editor.show_local_selections = true; - })?; - window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone())) }) - .detach_and_log_err(cx); +} + +pub(crate) fn observe_insertion( + text: &Arc, + range_to_replace: Option>, + cx: &mut WindowContext, +) { + Vim::update(cx, |vim, _| { + if vim.workspace_state.ignore_current_insertion { + vim.workspace_state.ignore_current_insertion = false; + return; + } + if vim.workspace_state.dot_recording { + vim.workspace_state + .recorded_actions + .push(ReplayableAction::Insertion { + text: text.clone(), + utf16_range_to_replace: range_to_replace.clone(), + }); + if vim.workspace_state.stop_recording_after_next_action { + vim.workspace_state.dot_recording = false; + vim.workspace_state.stop_recording_after_next_action = false; + } + } + if let Some(recording_register) = vim.workspace_state.recording_register { + vim.workspace_state + .recordings + .entry(recording_register) + .or_default() + .push(ReplayableAction::Insertion { + text: text.clone(), + utf16_range_to_replace: range_to_replace, + }); + } + }); } #[cfg(test)] @@ -510,4 +680,76 @@ mod test { cx.simulate_shared_keystrokes("u").await; cx.shared_state().await.assert_eq("hellˇo"); } + + #[gpui::test] + async fn test_record_replay(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world").await; + cx.simulate_shared_keystrokes("q w c w j escape q").await; + cx.shared_state().await.assert_eq("ˇj world"); + cx.simulate_shared_keystrokes("2 l @ w").await; + cx.shared_state().await.assert_eq("j ˇj"); + } + + #[gpui::test] + async fn test_record_replay_count(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world!!").await; + cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q") + .await; + cx.shared_state().await.assert_eq("0ˇo world!!"); + cx.simulate_shared_keystrokes("2 @ a").await; + cx.shared_state().await.assert_eq("000ˇ!"); + } + + #[gpui::test] + async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world").await; + cx.simulate_shared_keystrokes("q a r a l r b l q").await; + cx.shared_state().await.assert_eq("abˇllo world"); + cx.simulate_shared_keystrokes(".").await; + cx.shared_state().await.assert_eq("abˇblo world"); + cx.simulate_shared_keystrokes("shift-q").await; + cx.shared_state().await.assert_eq("ababˇo world"); + cx.simulate_shared_keystrokes(".").await; + cx.shared_state().await.assert_eq("ababˇb world"); + } + + #[gpui::test] + async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world").await; + cx.simulate_shared_keystrokes("r o q w . q").await; + cx.shared_state().await.assert_eq("ˇoello world"); + cx.simulate_shared_keystrokes("d l").await; + cx.shared_state().await.assert_eq("ˇello world"); + cx.simulate_shared_keystrokes("@ w").await; + cx.shared_state().await.assert_eq("ˇllo world"); + } + + #[gpui::test] + async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇhello world").await; + cx.simulate_shared_keystrokes("q z r a l q").await; + cx.shared_state().await.assert_eq("aˇello world"); + cx.simulate_shared_keystrokes("q b @ z @ z q").await; + cx.shared_state().await.assert_eq("aaaˇlo world"); + cx.simulate_shared_keystrokes("@ @").await; + cx.shared_state().await.assert_eq("aaaaˇo world"); + cx.simulate_shared_keystrokes("@ b").await; + cx.shared_state().await.assert_eq("aaaaaaˇworld"); + cx.simulate_shared_keystrokes("@ @").await; + cx.shared_state().await.assert_eq("aaaaaaaˇorld"); + cx.simulate_shared_keystrokes("q z r b l q").await; + cx.shared_state().await.assert_eq("aaaaaaabˇrld"); + cx.simulate_shared_keystrokes("@ b").await; + cx.shared_state().await.assert_eq("aaaaaaabbbˇd"); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index eef88e297b072d..8c724228a91f71 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,5 +1,6 @@ use std::{fmt::Display, ops::Range, sync::Arc}; +use crate::normal::repeat::Replayer; use crate::surrounds::SurroundsType; use crate::{motion::Motion, object::Object}; use collections::HashMap; @@ -68,6 +69,8 @@ pub enum Operator { Uppercase, OppositeCase, Register, + RecordRegister, + ReplayRegister, } #[derive(Default, Clone)] @@ -155,15 +158,23 @@ impl From for Register { pub struct WorkspaceState { pub last_find: Option, - pub recording: bool, + pub dot_recording: bool, + pub dot_replaying: bool, + pub stop_recording_after_next_action: bool, - pub replaying: bool, + pub ignore_current_insertion: bool, pub recorded_count: Option, pub recorded_actions: Vec, pub recorded_selection: RecordedSelection, + pub recording_register: Option, + pub last_recorded_register: Option, + pub last_replayed_register: Option, + pub replayer: Option, + pub last_yank: Option, pub registers: HashMap, + pub recordings: HashMap>, } #[derive(Debug)] @@ -228,6 +239,8 @@ impl EditorState { | Some(Operator::FindBackward { .. }) | Some(Operator::Mark) | Some(Operator::Register) + | Some(Operator::RecordRegister) + | Some(Operator::ReplayRegister) | Some(Operator::Jump { .. }) ) } @@ -322,6 +335,8 @@ impl Operator { Operator::Lowercase => "gu", Operator::OppositeCase => "g~", Operator::Register => "\"", + Operator::RecordRegister => "q", + Operator::ReplayRegister => "@", } } @@ -333,6 +348,8 @@ impl Operator { | Operator::Jump { .. } | Operator::FindBackward { .. } | Operator::Register + | Operator::RecordRegister + | Operator::ReplayRegister | Operator::Replace | Operator::AddSurrounds { target: Some(_) } | Operator::ChangeSurrounds { .. } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index db758c1feafaf9..be7244f7f0fd06 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -31,7 +31,11 @@ use gpui::{ use language::{CursorShape, Point, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; -use normal::{mark::create_visual_marks, normal_replace}; +use normal::{ + mark::create_visual_marks, + normal_replace, + repeat::{observe_action, observe_insertion, record_register, replay_register}, +}; use replace::multi_replace; use schemars::JsonSchema; use serde::Deserialize; @@ -170,18 +174,7 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) .as_ref() .map(|action| action.boxed_clone()) { - Vim::update(cx, |vim, _| { - if vim.workspace_state.recording { - vim.workspace_state - .recorded_actions - .push(ReplayableAction::Action(action.boxed_clone())); - - if vim.workspace_state.stop_recording_after_next_action { - vim.workspace_state.recording = false; - vim.workspace_state.stop_recording_after_next_action = false; - } - } - }); + observe_action(action.boxed_clone(), cx); // Keystroke is handled by the vim system, so continue forward if action.name().starts_with("vim::") { @@ -201,7 +194,9 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) | Operator::DeleteSurrounds | Operator::Mark | Operator::Jump { .. } - | Operator::Register, + | Operator::Register + | Operator::RecordRegister + | Operator::ReplayRegister, ) => {} Some(_) => { vim.clear_operator(cx); @@ -254,12 +249,12 @@ impl Vim { } EditorEvent::InputIgnored { text } => { Vim::active_editor_input_ignored(text.clone(), cx); - Vim::record_insertion(text, None, cx) + observe_insertion(text, None, cx) } EditorEvent::InputHandled { text, utf16_range_to_replace: range_to_replace, - } => Vim::record_insertion(text, range_to_replace.clone(), cx), + } => observe_insertion(text, range_to_replace.clone(), cx), EditorEvent::TransactionBegun { transaction_id } => Vim::update(cx, |vim, cx| { vim.transaction_begun(*transaction_id, cx); }), @@ -288,27 +283,6 @@ impl Vim { self.sync_vim_settings(cx); } - fn record_insertion( - text: &Arc, - range_to_replace: Option>, - cx: &mut WindowContext, - ) { - Vim::update(cx, |vim, _| { - if vim.workspace_state.recording { - vim.workspace_state - .recorded_actions - .push(ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace, - }); - if vim.workspace_state.stop_recording_after_next_action { - vim.workspace_state.recording = false; - vim.workspace_state.stop_recording_after_next_action = false; - } - } - }); - } - fn update_active_editor( &mut self, cx: &mut WindowContext, @@ -333,8 +307,8 @@ impl Vim { /// When doing an action that modifies the buffer, we start recording so that `.` /// will replay the action. pub fn start_recording(&mut self, cx: &mut WindowContext) { - if !self.workspace_state.replaying { - self.workspace_state.recording = true; + if !self.workspace_state.dot_replaying { + self.workspace_state.dot_recording = true; self.workspace_state.recorded_actions = Default::default(); self.workspace_state.recorded_count = None; @@ -376,15 +350,18 @@ impl Vim { } } - pub fn stop_replaying(&mut self) { - self.workspace_state.replaying = false; + pub fn stop_replaying(&mut self, _: &mut WindowContext) { + self.workspace_state.dot_replaying = false; + if let Some(replayer) = self.workspace_state.replayer.take() { + replayer.stop(); + } } /// When finishing an action that modifies the buffer, stop recording. /// as you usually call this within a keystroke handler we also ensure that /// the current action is recorded. pub fn stop_recording(&mut self) { - if self.workspace_state.recording { + if self.workspace_state.dot_recording { self.workspace_state.stop_recording_after_next_action = true; } } @@ -394,11 +371,11 @@ impl Vim { /// /// This doesn't include the current action. pub fn stop_recording_immediately(&mut self, action: Box) { - if self.workspace_state.recording { + if self.workspace_state.dot_recording { self.workspace_state .recorded_actions .push(ReplayableAction::Action(action.boxed_clone())); - self.workspace_state.recording = false; + self.workspace_state.dot_recording = false; self.workspace_state.stop_recording_after_next_action = false; } } @@ -511,7 +488,7 @@ impl Vim { } fn take_count(&mut self, cx: &mut WindowContext) -> Option { - if self.workspace_state.replaying { + if self.workspace_state.dot_replaying { return self.workspace_state.recorded_count; } @@ -522,7 +499,7 @@ impl Vim { state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1) })) }; - if self.workspace_state.recording { + if self.workspace_state.dot_recording { self.workspace_state.recorded_count = count; } self.sync_vim_settings(cx); @@ -898,6 +875,8 @@ impl Vim { Some(Operator::Mark) => Vim::update(cx, |vim, cx| { normal::mark::create_mark(vim, text, false, cx) }), + Some(Operator::RecordRegister) => record_register(text.chars().next().unwrap(), cx), + Some(Operator::ReplayRegister) => replay_register(text.chars().next().unwrap(), cx), Some(Operator::Register) => Vim::update(cx, |vim, cx| match vim.state().mode { Mode::Insert => { vim.update_active_editor(cx, |vim, editor, cx| { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index e6f5d295600ff8..379e2972b4b2fd 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -610,7 +610,7 @@ pub fn select_match( }); if !match_exists { vim.clear_operator(cx); - vim.stop_replaying(); + vim.stop_replaying(cx); return; } vim.update_active_editor(cx, |_, editor, cx| { diff --git a/crates/vim/test_data/test_record_replay.json b/crates/vim/test_data/test_record_replay.json new file mode 100644 index 00000000000000..8346d9ad8b79c0 --- /dev/null +++ b/crates/vim/test_data/test_record_replay.json @@ -0,0 +1,14 @@ +{"Put":{"state":"ˇhello world"}} +{"Key":"q"} +{"Key":"w"} +{"Key":"c"} +{"Key":"w"} +{"Key":"j"} +{"Key":"escape"} +{"Key":"q"} +{"Get":{"state":"ˇj world","mode":"Normal"}} +{"Key":"2"} +{"Key":"l"} +{"Key":"@"} +{"Key":"w"} +{"Get":{"state":"j ˇj","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_count.json b/crates/vim/test_data/test_record_replay_count.json new file mode 100644 index 00000000000000..78023ef350a897 --- /dev/null +++ b/crates/vim/test_data/test_record_replay_count.json @@ -0,0 +1,16 @@ +{"Put":{"state":"ˇhello world!!"}} +{"Key":"q"} +{"Key":"a"} +{"Key":"v"} +{"Key":"3"} +{"Key":"l"} +{"Key":"s"} +{"Key":"0"} +{"Key":"escape"} +{"Key":"l"} +{"Key":"q"} +{"Get":{"state":"0ˇo world!!","mode":"Normal"}} +{"Key":"2"} +{"Key":"@"} +{"Key":"a"} +{"Get":{"state":"000ˇ!","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_dot.json b/crates/vim/test_data/test_record_replay_dot.json new file mode 100644 index 00000000000000..9cc565f16030a8 --- /dev/null +++ b/crates/vim/test_data/test_record_replay_dot.json @@ -0,0 +1,17 @@ +{"Put":{"state":"ˇhello world"}} +{"Key":"q"} +{"Key":"a"} +{"Key":"r"} +{"Key":"a"} +{"Key":"l"} +{"Key":"r"} +{"Key":"b"} +{"Key":"l"} +{"Key":"q"} +{"Get":{"state":"abˇllo world","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"abˇblo world","mode":"Normal"}} +{"Key":"shift-q"} +{"Get":{"state":"ababˇo world","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"ababˇb world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_interleaved.json b/crates/vim/test_data/test_record_replay_interleaved.json new file mode 100644 index 00000000000000..aefb5eac2a18a0 --- /dev/null +++ b/crates/vim/test_data/test_record_replay_interleaved.json @@ -0,0 +1,35 @@ +{"Put":{"state":"ˇhello world"}} +{"Key":"q"} +{"Key":"z"} +{"Key":"r"} +{"Key":"a"} +{"Key":"l"} +{"Key":"q"} +{"Get":{"state":"aˇello world","mode":"Normal"}} +{"Key":"q"} +{"Key":"b"} +{"Key":"@"} +{"Key":"z"} +{"Key":"@"} +{"Key":"z"} +{"Key":"q"} +{"Get":{"state":"aaaˇlo world","mode":"Normal"}} +{"Key":"@"} +{"Key":"@"} +{"Get":{"state":"aaaaˇo world","mode":"Normal"}} +{"Key":"@"} +{"Key":"b"} +{"Get":{"state":"aaaaaaˇworld","mode":"Normal"}} +{"Key":"@"} +{"Key":"@"} +{"Get":{"state":"aaaaaaaˇorld","mode":"Normal"}} +{"Key":"q"} +{"Key":"z"} +{"Key":"r"} +{"Key":"b"} +{"Key":"l"} +{"Key":"q"} +{"Get":{"state":"aaaaaaabˇrld","mode":"Normal"}} +{"Key":"@"} +{"Key":"b"} +{"Get":{"state":"aaaaaaabbbˇd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_of_dot.json b/crates/vim/test_data/test_record_replay_of_dot.json new file mode 100644 index 00000000000000..f4cce4bb3d7ffe --- /dev/null +++ b/crates/vim/test_data/test_record_replay_of_dot.json @@ -0,0 +1,14 @@ +{"Put":{"state":"ˇhello world"}} +{"Key":"r"} +{"Key":"o"} +{"Key":"q"} +{"Key":"w"} +{"Key":"."} +{"Key":"q"} +{"Get":{"state":"ˇoello world","mode":"Normal"}} +{"Key":"d"} +{"Key":"l"} +{"Get":{"state":"ˇello world","mode":"Normal"}} +{"Key":"@"} +{"Key":"w"} +{"Get":{"state":"ˇllo world","mode":"Normal"}} diff --git a/docs/src/vim.md b/docs/src/vim.md index 4718129ef74e5d..e8289ee780637b 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -6,7 +6,7 @@ Zed includes a vim emulation layer known as "vim mode". This document aims to de Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. -This means Zed will never be 100% Vim compatible, but should be 100% Vim familiar! We expect that our Vim mode already copes with 90% of your workflow, and we'd like to keep improving it. If you find things that you can’t yet do in Vim mode, but which you rely on in your current workflow, please leave feedback in the editor itself (`:feedback`), or [file an issue](https://github.com/zed-industries/zed/issues). +This means Zed will never be 100% Vim compatible, but should be 100% Vim familiar! We expect that our Vim mode already copes with 90% of your workflow, and we'd like to keep improving it. If you find things that you can’t yet do in Vim mode, but which you rely on in your current workflow, please [file an issue](https://github.com/zed-industries/zed/issues). ## Zed-specific features @@ -78,6 +78,8 @@ Vim mode uses Zed to define concepts like "brackets" (for the `%` key) and "word Vim mode emulates visual block mode using Zed's multiple cursor support. This again leads to some differences, but is much more powerful. +Vim's macro support (`q` and `@`) is implemented using Zed's actions. This lets us support recording and replaying of autocompleted code, etc. Unlike Vim, Zed does not re-use the yank registers for recording macros, they are two separate namespaces. + Finally, Vim mode's search and replace functionality is backed by Zed's. This means that the pattern syntax is slightly different, see the section on [Regex differences](#regex-differences) for details. ## Custom key bindings From 64755a7aeaeb18defb12f0c78a42148038cab560 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 3 Jul 2024 17:05:26 +0200 Subject: [PATCH 06/40] linux/x11: Custom run loop with `mio` instead of `calloop` (#13646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes the implementation of the X11 client to use `mio`, as a polling mechanism, and a custom run loop instead of `calloop` and its callback-based approach. We're doing this for one big reason: more control over how we handle events. With `calloop` we don't have any control over which events are processed when and how long they're processes for. For example: we could be blasted with 150 input events from X11 and miss a frame while processing them, but instead of then drawing a new frame, calloop could decide to work off the runnables that were generated from application-level code, which would then again cause us to be behind. We kinda worked around some of that in https://github.com/zed-industries/zed/pull/12839 but the problem still persists. So what we're doing here is to use `mio` as a polling-mechanism. `mio` notifies us if there are X11 on the XCB connection socket to be processed. We also use its timeout mechanism to make sure that we don't wait for events when we should render frames. On top of `mio` we now have a custom run loop that allows us to decide how much time to spend on what — input events, rendering windows, XDG events, runnables — and in what order we work things off. This custom run loop is consciously "dumb": we render all windows at the highest frame rate right now, because we want to keep things predictable for now while we test this approach more. We can then always switch to more granular timings. But considering that our loop runs and checks for windows to be redrawn whenever there's an event, this is more an optimization than a requirement. One reason for why we're doing this for X11 but not for Wayland is due to how peculiar X11's event handling is: it's asynchronous and by default X11 generates synthetic events when a key is held down. That can lead to us being flooded with input events if someone keeps a key pressed. So another optimization that's in here is inspired by [GLFW's X11 input handling](https://github.com/glfw/glfw/blob/b35641f4a3c62aa86a0b3c983d163bc0fe36026d/src/x11_window.c#L1321-L1349): based on a heuristic we detect whether a `KeyRelease` event was auto-generated and if so, we drop it. That essentially halves the amount of events we have to process when someone keeps a key pressed. Release Notes: - N/A --------- Co-authored-by: Conrad Irwin Co-authored-by: Conrad --- Cargo.lock | 28 +- crates/gpui/Cargo.toml | 1 + crates/gpui/src/platform/linux/dispatcher.rs | 10 +- .../src/platform/linux/headless/client.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 24 +- .../gpui/src/platform/linux/wayland/client.rs | 6 +- crates/gpui/src/platform/linux/x11/client.rs | 491 ++++++++++-------- crates/gpui/src/platform/linux/x11/window.rs | 48 +- .../src/platform/linux/xdg_desktop_portal.rs | 52 +- 9 files changed, 424 insertions(+), 238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c75f9ff865cd8..14c6edd672d674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4889,6 +4889,7 @@ dependencies = [ "log", "media", "metal", + "mio 1.0.0", "num_cpus", "objc", "oo7", @@ -5143,9 +5144,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -5659,7 +5660,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -5690,7 +5691,7 @@ dependencies = [ "fnv", "lazy_static", "libc", - "mio", + "mio 0.8.11", "rand 0.8.5", "serde", "tempfile", @@ -6639,6 +6640,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "miow" version = "0.6.0" @@ -6877,7 +6891,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 0.8.11", "walkdir", "windows-sys 0.48.0", ] @@ -7057,7 +7071,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] @@ -11098,7 +11112,7 @@ dependencies = [ "backtrace", "bytes 1.5.0", "libc", - "mio", + "mio 0.8.11", "num_cpus", "parking_lot", "pin-project-lite", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 0781c7cdd6bf3c..c7c3a92a953997 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -141,6 +141,7 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca ] } font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] } x11-clipboard = "0.9.2" +mio = { version = "1.0.0", features = ["os-poll", "os-ext"] } [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/crates/gpui/src/platform/linux/dispatcher.rs index 0047b113a80041..ad4628ed5d5797 100644 --- a/crates/gpui/src/platform/linux/dispatcher.rs +++ b/crates/gpui/src/platform/linux/dispatcher.rs @@ -5,9 +5,10 @@ use calloop::{ timer::TimeoutAction, EventLoop, }; +use mio::Waker; use parking::{Parker, Unparker}; use parking_lot::Mutex; -use std::{thread, time::Duration}; +use std::{sync::Arc, thread, time::Duration}; use util::ResultExt; struct TimerAfter { @@ -18,6 +19,7 @@ struct TimerAfter { pub(crate) struct LinuxDispatcher { parker: Mutex, main_sender: Sender, + main_waker: Option>, timer_sender: Sender, background_sender: flume::Sender, _background_threads: Vec>, @@ -25,7 +27,7 @@ pub(crate) struct LinuxDispatcher { } impl LinuxDispatcher { - pub fn new(main_sender: Sender) -> Self { + pub fn new(main_sender: Sender, main_waker: Option>) -> Self { let (background_sender, background_receiver) = flume::unbounded::(); let thread_count = std::thread::available_parallelism() .map(|i| i.get()) @@ -77,6 +79,7 @@ impl LinuxDispatcher { Self { parker: Mutex::new(Parker::new()), main_sender, + main_waker, timer_sender, background_sender, _background_threads: background_threads, @@ -96,6 +99,9 @@ impl PlatformDispatcher for LinuxDispatcher { fn dispatch_on_main_thread(&self, runnable: Runnable) { self.main_sender.send(runnable).ok(); + if let Some(main_waker) = self.main_waker.as_ref() { + main_waker.wake().ok(); + } } fn dispatch_after(&self, duration: Duration, runnable: Runnable) { diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index bb066684f836d1..7f9a623a6e592d 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -22,7 +22,7 @@ impl HeadlessClient { pub(crate) fn new() -> Self { let event_loop = EventLoop::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None); let handle = event_loop.handle(); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 3c3be3f6cdf5df..54fc4aa17d59b0 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -26,6 +26,7 @@ use calloop::{EventLoop, LoopHandle, LoopSignal}; use filedescriptor::FileDescriptor; use flume::{Receiver, Sender}; use futures::channel::oneshot; +use mio::Waker; use parking_lot::Mutex; use time::UtcOffset; use util::ResultExt; @@ -84,6 +85,16 @@ pub(crate) struct PlatformHandlers { pub(crate) validate_app_menu_command: Option bool>>, } +pub trait QuitSignal { + fn quit(&mut self); +} + +impl QuitSignal for LoopSignal { + fn quit(&mut self) { + self.stop(); + } +} + pub(crate) struct LinuxCommon { pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, @@ -91,17 +102,20 @@ pub(crate) struct LinuxCommon { pub(crate) appearance: WindowAppearance, pub(crate) auto_hide_scrollbars: bool, pub(crate) callbacks: PlatformHandlers, - pub(crate) signal: LoopSignal, + pub(crate) quit_signal: Box, pub(crate) menus: Vec, } impl LinuxCommon { - pub fn new(signal: LoopSignal) -> (Self, Channel) { + pub fn new( + quit_signal: Box, + main_waker: Option>, + ) -> (Self, Channel) { let (main_sender, main_receiver) = calloop::channel::channel::(); let text_system = Arc::new(CosmicTextSystem::new()); let callbacks = PlatformHandlers::default(); - let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker)); let background_executor = BackgroundExecutor::new(dispatcher.clone()); @@ -112,7 +126,7 @@ impl LinuxCommon { appearance: WindowAppearance::Light, auto_hide_scrollbars: false, callbacks, - signal, + quit_signal, menus: Vec::new(), }; @@ -146,7 +160,7 @@ impl Platform for P { } fn quit(&self) { - self.with_common(|common| common.signal.stop()); + self.with_common(|common| common.quit_signal.quit()); } fn compositor_name(&self) -> &'static str { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 250df50875c35a..22750d1814d62e 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -310,7 +310,7 @@ impl WaylandClientStatePtr { } } if state.windows.is_empty() { - state.common.signal.stop(); + state.common.quit_signal.quit(); } } } @@ -406,7 +406,7 @@ impl WaylandClient { let event_loop = EventLoop::::try_new().unwrap(); - let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal()); + let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None); let handle = event_loop.handle(); handle @@ -443,7 +443,7 @@ impl WaylandClient { let mut cursor = Cursor::new(&conn, &globals, 24); handle - .insert_source(XDPEventSource::new(&common.background_executor), { + .insert_source(XDPEventSource::new(&common.background_executor, None), { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { if let Some(client) = client.0.upgrade() { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index ab0b9a79b1f962..ba42bc3fa4f49f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,19 +1,23 @@ use std::cell::RefCell; use std::collections::HashSet; use std::ops::Deref; +use std::os::fd::AsRawFd; use std::rc::{Rc, Weak}; +use std::sync::Arc; use std::time::{Duration, Instant}; -use calloop::generic::{FdWrapper, Generic}; -use calloop::{EventLoop, LoopHandle, RegistrationToken}; +use anyhow::Context; +use async_task::Runnable; +use calloop::channel::Channel; use collections::HashMap; -use util::ResultExt; +use futures::channel::oneshot; +use mio::{Interest, Token, Waker}; +use util::ResultExt; use x11rb::connection::{Connection, RequestConnection}; use x11rb::cursor; use x11rb::errors::ConnectionError; -use x11rb::protocol::randr::ConnectionExt as _; use x11rb::protocol::xinput::ConnectionExt; use x11rb::protocol::xkb::ConnectionExt as _; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _}; @@ -30,7 +34,7 @@ use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, - Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; use super::{ @@ -47,7 +51,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; pub(crate) struct WindowRef { window: X11WindowStatePtr, - refresh_event_token: RegistrationToken, } impl WindowRef { @@ -95,15 +98,18 @@ impl From for EventHandlerError { } pub struct X11ClientState { - pub(crate) loop_handle: LoopHandle<'static, X11Client>, - pub(crate) event_loop: Option>, + /// poll is in an Option so we can take it out in `run()` without + /// mutating self. + poll: Option, + quit_signal_rx: oneshot::Receiver<()>, + runnables: Channel, + xdp_event_source: XDPEventSource, pub(crate) last_click: Instant, pub(crate) last_location: Point, pub(crate) current_count: usize, pub(crate) scale_factor: f32, - pub(crate) xcb_connection: Rc, pub(crate) x_root_index: usize, pub(crate) _resource_database: Database, @@ -139,14 +145,46 @@ impl X11ClientStatePtr { 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.remove(&x_window).is_none() { + log::warn!( + "failed to remove X window {} from client state, does not exist", + x_window + ); } state.cursor_styles.remove(&x_window); if state.windows.is_empty() { - state.common.signal.stop(); + state.common.quit_signal.quit(); + } + } +} + +struct ChannelQuitSignal { + tx: Option>, + waker: Option>, +} + +impl ChannelQuitSignal { + fn new(waker: Option>) -> (Self, oneshot::Receiver<()>) { + let (tx, rx) = oneshot::channel::<()>(); + + let quit_signal = ChannelQuitSignal { + tx: Some(tx), + waker, + }; + + (quit_signal, rx) + } +} + +impl QuitSignal for ChannelQuitSignal { + fn quit(&mut self) { + if let Some(tx) = self.tx.take() { + tx.send(()).log_err(); + if let Some(waker) = self.waker.as_ref() { + waker.wake().ok(); + } } } } @@ -156,27 +194,12 @@ pub(crate) struct X11Client(Rc>); impl X11Client { pub(crate) fn new() -> Self { - 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, { - let handle = handle.clone(); - move |event, _, _: &mut X11Client| { - if let calloop::channel::Event::Msg(runnable) = event { - // Insert the runnables as idle callbacks, so we make sure that user-input and X11 - // events have higher priority and runnables are only worked off after the event - // callbacks. - handle.insert_idle(|_| { - runnable.run(); - }); - } - } - }) - .unwrap(); + let mut poll = mio::Poll::new().unwrap(); + + let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap()); + + let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone())); + let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone())); let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap(); xcb_connection @@ -275,105 +298,18 @@ impl X11Client { None }; - // Safety: Safe if xcb::Connection always returns a valid fd - let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) }; - - handle - .insert_source( - Generic::new_with_error::( - fd, - calloop::Interest::READ, - calloop::Mode::Level, - ), - { - let xcb_connection = xcb_connection.clone(); - move |_readiness, _, client| { - let mut events = Vec::new(); - let mut windows_to_refresh = HashSet::new(); - - while let Some(event) = xcb_connection.poll_for_event()? { - if let Event::Expose(event) = event { - windows_to_refresh.insert(event.window); - } else { - events.push(event); - } - } - - for window in windows_to_refresh.into_iter() { - if let Some(window) = client.get_window(window) { - window.refresh(); - } - } - - for event in events.into_iter() { - let mut state = client.0.borrow_mut(); - if state.ximc.is_none() || state.xim_handler.is_none() { - drop(state); - client.handle_event(event); - continue; - } - - let mut ximc = state.ximc.take().unwrap(); - let mut xim_handler = state.xim_handler.take().unwrap(); - let xim_connected = xim_handler.connected; - drop(state); - - let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { - Ok(handled) => handled, - Err(err) => { - log::error!("XIMClientError: {}", err); - false - } - }; - let xim_callback_event = xim_handler.last_callback_event.take(); - - let mut state = client.0.borrow_mut(); - state.ximc = Some(ximc); - state.xim_handler = Some(xim_handler); - drop(state); - - if let Some(event) = xim_callback_event { - client.handle_xim_callback_event(event); - } - - if xim_filtered { - continue; - } - - if xim_connected { - client.xim_handle_event(event); - } else { - client.handle_event(event); - } - } + let xdp_event_source = + XDPEventSource::new(&common.background_executor, Some(waker.clone())); - Ok(calloop::PostAction::Continue) - } - }, - ) - .expect("Failed to initialize x11 event source"); + X11Client(Rc::new(RefCell::new(X11ClientState { + poll: Some(poll), + runnables, - handle - .insert_source(XDPEventSource::new(&common.background_executor), { - move |event, _, client| match event { - XDPEvent::WindowAppearance(appearance) => { - client.with_common(|common| common.appearance = appearance); - for (_, window) in &mut client.0.borrow_mut().windows { - window.window.set_appearance(appearance); - } - } - XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { - // noop, X11 manages this for us. - } - } - }) - .unwrap(); + xdp_event_source, + quit_signal_rx, + common, - X11Client(Rc::new(RefCell::new(X11ClientState { modifiers: Modifiers::default(), - event_loop: Some(event_loop), - loop_handle: handle, - common, last_click: Instant::now(), last_location: Point::new(px(0.0), px(0.0)), current_count: 0, @@ -468,6 +404,110 @@ impl X11Client { .map(|window_reference| window_reference.window.clone()) } + fn read_x11_events(&self) -> (HashSet, Vec) { + let mut events = Vec::new(); + let mut windows_to_refresh = HashSet::new(); + let mut state = self.0.borrow_mut(); + + let mut last_key_release: Option = None; + + loop { + match state.xcb_connection.poll_for_event() { + Ok(Some(event)) => { + if let Event::Expose(expose_event) = event { + windows_to_refresh.insert(expose_event.window); + } else { + match event { + Event::KeyRelease(_) => { + last_key_release = Some(event); + } + Event::KeyPress(key_press) => { + if let Some(Event::KeyRelease(key_release)) = + last_key_release.take() + { + // We ignore that last KeyRelease if it's too close to this KeyPress, + // suggesting that it's auto-generated by X11 as a key-repeat event. + if key_release.detail != key_press.detail + || key_press.time.wrapping_sub(key_release.time) > 20 + { + events.push(Event::KeyRelease(key_release)); + } + } + events.push(Event::KeyPress(key_press)); + } + _ => { + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + events.push(event); + } + } + } + } + Ok(None) => { + // Add any remaining stored KeyRelease event + if let Some(release_event) = last_key_release.take() { + events.push(release_event); + } + break; + } + Err(e) => { + log::warn!("error polling for X11 events: {e:?}"); + break; + } + } + } + + (windows_to_refresh, events) + } + + fn process_x11_events(&self, events: Vec) { + for event in events.into_iter() { + let mut state = self.0.borrow_mut(); + if state.ximc.is_none() || state.xim_handler.is_none() { + drop(state); + self.handle_event(event); + continue; + } + + let mut ximc = state.ximc.take().unwrap(); + let mut xim_handler = state.xim_handler.take().unwrap(); + let xim_connected = xim_handler.connected; + drop(state); + + // let xim_filtered = false; + let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { + Ok(handled) => handled, + Err(err) => { + log::error!("XIMClientError: {}", err); + false + } + }; + let xim_callback_event = xim_handler.last_callback_event.take(); + + let mut state = self.0.borrow_mut(); + state.ximc = Some(ximc); + state.xim_handler = Some(xim_handler); + + if let Some(event) = xim_callback_event { + drop(state); + self.handle_xim_callback_event(event); + } else { + drop(state); + } + + if xim_filtered { + continue; + } + + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); + } + } + } + fn handle_event(&self, event: Event) -> Option<()> { match event { Event::ClientMessage(event) => { @@ -902,11 +942,13 @@ impl X11Client { } } +const XCB_CONNECTION_TOKEN: Token = Token(0); +const WAKER_TOKEN: Token = Token(1); + impl LinuxClient for X11Client { fn compositor_name(&self) -> &'static str { "X11" } - fn with_common(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R { f(&mut self.0.borrow_mut().common) } @@ -972,69 +1014,8 @@ impl LinuxClient for X11Client { state.common.appearance, )?; - let screen_resources = state - .xcb_connection - .randr_get_screen_resources(x_window) - .unwrap() - .reply() - .expect("Could not find available screens"); - - let mode = screen_resources - .crtcs - .iter() - .find_map(|crtc| { - let crtc_info = state - .xcb_connection - .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) - .ok()? - .reply() - .ok()?; - - screen_resources - .modes - .iter() - .find(|m| m.id == crtc_info.mode) - }) - .expect("Unable to find screen refresh rate"); - - let refresh_event_token = state - .loop_handle - .insert_source(calloop::timer::Timer::immediate(), { - let refresh_duration = mode_refresh_rate(mode); - move |mut instant, (), client| { - let state = client.0.borrow_mut(); - state - .xcb_connection - .send_event( - false, - x_window, - xproto::EventMask::EXPOSURE, - xproto::ExposeEvent { - response_type: xproto::EXPOSE_EVENT, - sequence: 0, - window: x_window, - x: 0, - y: 0, - width: 0, - height: 0, - count: 1, - }, - ) - .unwrap(); - let _ = state.xcb_connection.flush().unwrap(); - // Take into account that some frames have been skipped - let now = Instant::now(); - while instant < now { - instant += refresh_duration; - } - calloop::timer::TimeoutAction::ToInstant(instant) - } - }) - .expect("Failed to initialize refresh timer"); - let window_ref = WindowRef { window: window.0.clone(), - refresh_event_token, }; state.windows.insert(x_window, window_ref); @@ -1157,14 +1138,123 @@ impl LinuxClient for X11Client { } fn run(&self) { - let mut event_loop = self + let mut poll = self .0 .borrow_mut() - .event_loop + .poll .take() - .expect("App is already running"); + .context("no poll set on X11Client. calling run more than once is not possible") + .unwrap(); + + let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd(); + let mut xcb_source = mio::unix::SourceFd(&xcb_fd); + poll.registry() + .register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE) + .unwrap(); + + let mut events = mio::Events::with_capacity(1024); + let mut next_refresh_needed = Instant::now(); + + 'run_loop: loop { + let poll_timeout = next_refresh_needed - Instant::now(); + // We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds + let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64); + + if poll_timeout >= Duration::from_millis(1) { + let _ = poll.poll(&mut events, Some(poll_timeout)); + }; + + let mut state = self.0.borrow_mut(); - event_loop.run(None, &mut self.clone(), |_| {}).log_err(); + // Check if we need to quit + if let Ok(Some(())) = state.quit_signal_rx.try_recv() { + return; + } + + // Redraw windows + let now = Instant::now(); + if now > next_refresh_needed { + // This will be pulled down to 16ms (or less) if a window is open + let mut frame_length = Duration::from_millis(100); + + let mut windows = vec![]; + for (_, window_ref) in state.windows.iter() { + if !window_ref.window.state.borrow().destroyed { + frame_length = frame_length.min(window_ref.window.refresh_rate()); + windows.push(window_ref.window.clone()); + } + } + + drop(state); + + for window in windows { + window.refresh(); + } + + state = self.0.borrow_mut(); + + // In the case that we're looping a bit too fast, slow down + next_refresh_needed = now.max(next_refresh_needed) + frame_length; + } + + // X11 events + drop(state); + + loop { + let (x_windows, events) = self.read_x11_events(); + for x_window in x_windows { + if let Some(window) = self.get_window(x_window) { + window.refresh(); + } + } + + if events.len() == 0 { + break; + } + self.process_x11_events(events); + + // When X11 is sending us events faster than we can handle we'll + // let the frame rate drop to 10fps to try and avoid getting too behind. + if Instant::now() > next_refresh_needed + Duration::from_millis(80) { + continue 'run_loop; + } + } + + state = self.0.borrow_mut(); + + // Runnables + while let Ok(runnable) = state.runnables.try_recv() { + drop(state); + runnable.run(); + state = self.0.borrow_mut(); + + if Instant::now() + Duration::from_millis(1) >= next_refresh_needed { + continue 'run_loop; + } + } + + // XDG events + if let Ok(event) = state.xdp_event_source.try_recv() { + match event { + XDPEvent::WindowAppearance(appearance) => { + let mut windows = state + .windows + .values() + .map(|window| window.window.clone()) + .collect::>(); + drop(state); + + self.with_common(|common| common.appearance = appearance); + for mut window in windows { + window.set_appearance(appearance); + } + } + XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => { + // noop, X11 manages this for us. + } + }; + }; + } } fn active_window(&self) -> Option { @@ -1178,19 +1268,6 @@ impl LinuxClient for X11Client { } } -// Adatpted from: -// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 -pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { - if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { - return Duration::from_millis(16); - } - - let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); - let micros = 1_000_000_000 / millihertz; - log::info!("Refreshing at {} micros", micros); - Duration::from_micros(micros) -} - fn fp3232_to_f32(value: xinput::Fp3232) -> f32 { value.integral as f32 + value.frac as f32 / u32::MAX as f32 } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index d77be6f34c552a..b15de3df7327e5 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -14,6 +14,7 @@ use util::{maybe, ResultExt}; use x11rb::{ connection::Connection, protocol::{ + randr::{self, ConnectionExt as _}, xinput::{self, ConnectionExt as _}, xproto::{ self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply, @@ -31,6 +32,7 @@ use std::{ ptr::NonNull, rc::Rc, sync::{self, Arc}, + time::Duration, }; use super::{X11Display, XINPUT_MASTER_DEVICE}; @@ -159,6 +161,7 @@ pub struct Callbacks { pub struct X11WindowState { pub destroyed: bool, + refresh_rate: Duration, client: X11ClientStatePtr, executor: ForegroundExecutor, atoms: XcbAtoms, @@ -178,7 +181,7 @@ pub(crate) struct X11WindowStatePtr { pub state: Rc>, pub(crate) callbacks: Rc>, xcb_connection: Rc, - x_window: xproto::Window, + pub x_window: xproto::Window, } impl rwh::HasWindowHandle for RawWindow { @@ -397,6 +400,31 @@ impl X11WindowState { }; xcb_connection.map_window(x_window).unwrap(); + let screen_resources = xcb_connection + .randr_get_screen_resources(x_window) + .unwrap() + .reply() + .expect("Could not find available screens"); + + let mode = screen_resources + .crtcs + .iter() + .find_map(|crtc| { + let crtc_info = xcb_connection + .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME) + .ok()? + .reply() + .ok()?; + + screen_resources + .modes + .iter() + .find(|m| m.id == crtc_info.mode) + }) + .expect("Unable to find screen refresh rate"); + + let refresh_rate = mode_refresh_rate(&mode); + Ok(Self { client, executor, @@ -413,6 +441,7 @@ impl X11WindowState { appearance, handle, destroyed: false, + refresh_rate, }) } @@ -715,6 +744,10 @@ impl X11WindowStatePtr { (fun)() } } + + pub fn refresh_rate(&self) -> Duration { + self.state.borrow().refresh_rate + } } impl PlatformWindow for X11Window { @@ -1039,3 +1072,16 @@ impl PlatformWindow for X11Window { false } } + +// Adapted from: +// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111 +pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration { + if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 { + return Duration::from_millis(16); + } + + let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64); + let micros = 1_000_000_000 / millihertz; + log::info!("Refreshing at {} micros", micros); + Duration::from_micros(micros) +} diff --git a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs index b36e4826396ae2..4252f765835bfc 100644 --- a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs +++ b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs @@ -2,9 +2,13 @@ //! //! This module uses the [ashpd] crate +use std::sync::Arc; + +use anyhow::anyhow; use ashpd::desktop::settings::{ColorScheme, Settings}; -use calloop::channel::Channel; +use calloop::channel::{Channel, Sender}; use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory}; +use mio::Waker; use smol::stream::StreamExt; use crate::{BackgroundExecutor, WindowAppearance}; @@ -20,31 +24,45 @@ pub struct XDPEventSource { } impl XDPEventSource { - pub fn new(executor: &BackgroundExecutor) -> Self { + pub fn new(executor: &BackgroundExecutor, waker: Option>) -> Self { let (sender, channel) = calloop::channel::channel(); let background = executor.clone(); executor .spawn(async move { + fn send_event( + sender: &Sender, + waker: &Option>, + event: T, + ) -> Result<(), std::sync::mpsc::SendError> { + sender.send(event)?; + if let Some(waker) = waker { + waker.wake().ok(); + }; + Ok(()) + } + let settings = Settings::new().await?; if let Ok(initial_appearance) = settings.color_scheme().await { - sender.send(Event::WindowAppearance(WindowAppearance::from_native( - initial_appearance, - )))?; + send_event( + &sender, + &waker, + Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)), + )?; } if let Ok(initial_theme) = settings .read::("org.gnome.desktop.interface", "cursor-theme") .await { - sender.send(Event::CursorTheme(initial_theme))?; + send_event(&sender, &waker, Event::CursorTheme(initial_theme))?; } if let Ok(initial_size) = settings .read::("org.gnome.desktop.interface", "cursor-size") .await { - sender.send(Event::CursorSize(initial_size))?; + send_event(&sender, &waker, Event::CursorSize(initial_size))?; } if let Ok(mut cursor_theme_changed) = settings @@ -55,11 +73,12 @@ impl XDPEventSource { .await { let sender = sender.clone(); + let waker = waker.clone(); background .spawn(async move { while let Some(theme) = cursor_theme_changed.next().await { let theme = theme?; - sender.send(Event::CursorTheme(theme))?; + send_event(&sender, &waker, Event::CursorTheme(theme))?; } anyhow::Ok(()) }) @@ -74,11 +93,12 @@ impl XDPEventSource { .await { let sender = sender.clone(); + let waker = waker.clone(); background .spawn(async move { while let Some(size) = cursor_size_changed.next().await { let size = size?; - sender.send(Event::CursorSize(size))?; + send_event(&sender, &waker, Event::CursorSize(size))?; } anyhow::Ok(()) }) @@ -87,9 +107,11 @@ impl XDPEventSource { let mut appearance_changed = settings.receive_color_scheme_changed().await?; while let Some(scheme) = appearance_changed.next().await { - sender.send(Event::WindowAppearance(WindowAppearance::from_native( - scheme, - )))?; + send_event( + &sender, + &waker, + Event::WindowAppearance(WindowAppearance::from_native(scheme)), + )?; } anyhow::Ok(()) @@ -98,6 +120,12 @@ impl XDPEventSource { Self { channel } } + + pub fn try_recv(&self) -> anyhow::Result { + self.channel + .try_recv() + .map_err(|error| anyhow!("{}", error)) + } } impl EventSource for XDPEventSource { From 995b082c6487b1230d848623f443539fb1c7e1cb Mon Sep 17 00:00:00 2001 From: Allison Durham Date: Wed, 3 Jul 2024 10:07:11 -0500 Subject: [PATCH 07/40] Change `tool_calls` to be an Option in response (#13778) Here is an image of my now getting assistance responses! ![2024-07-03_08-45-37_swappy](https://github.com/zed-industries/zed/assets/20910163/904adc51-cb40-4622-878e-f679e0212426) I ended up adding a function to handle the use case of not serializing the tool_calls response if it is either null or empty to keep the functionality of the existing implementation (not deserializing if vec is empty). I'm sorta a noob, so happy to make changes if this isn't done correctly, although it does work and it does pass tests! Thanks a bunch to [amtoaer](https://github.com/amtoaer) for pointing me in the direction on how to fix it. Release Notes: - Fixed some responses being dropped from OpenAI-compatible providers ([#13741](https://github.com/zed-industries/zed/issues/13741)). --- crates/collab/src/rpc.rs | 1 + crates/open_ai/src/open_ai.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fb2788a3814549..61aaa98144b5c1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4462,6 +4462,7 @@ async fn complete_with_open_ai( tool_calls: choice .delta .tool_calls + .unwrap_or_default() .into_iter() .map(|delta| proto::ToolCallDelta { index: delta.index as u32, diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 0e4dc580fb52d2..df5d30786d884b 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -9,6 +9,10 @@ use strum::EnumIter; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; +fn is_none_or_empty, U>(opt: &Option) -> bool { + opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) +} + #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Role { @@ -182,8 +186,8 @@ pub struct FunctionContent { pub struct ResponseMessageDelta { pub role: Option, pub content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tool_calls: Vec, + #[serde(default, skip_serializing_if = "is_none_or_empty")] + pub tool_calls: Option>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] From 089cc85d4a09914808bde0849ee6ca6e7d11033a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 11:10:51 -0400 Subject: [PATCH 08/40] Use a dedicated test extension in extension tests (#13781) This PR updates the `extension` crate's tests to use a dedicated test extension for its tests instead of the real Gleam extension. As the Gleam extension continues to evolve, it makes it less suitable to use as a test fixture: 1. For a while now, the test has failed locally due to me having `gleam` on my $PATH, which causes the extension's `get_language_server_command` to go down a separate codepath. 2. With the addition of the `indexed_docs_providers` the test was hanging indefinitely. While these problems are likely solvable, it seems reasonable to have a dedicated extension to use as a test fixture. That way we can do whatever we need to exercise our test criteria. The `test-extension` is a fork of the Gleam extension with some additional functionality removed. Release Notes: - N/A --- Cargo.lock | 7 + Cargo.toml | 1 + crates/extension/src/extension_store_test.rs | 13 +- extensions/gleam/extension.toml | 2 +- extensions/test-extension/Cargo.toml | 16 ++ extensions/test-extension/LICENSE-APACHE | 1 + extensions/test-extension/README.md | 5 + extensions/test-extension/extension.toml | 15 ++ .../languages/gleam/config.toml | 12 ++ .../languages/gleam/highlights.scm | 130 ++++++++++++++ .../languages/gleam/indents.scm | 3 + .../languages/gleam/outline.scm | 31 ++++ .../test-extension/src/test_extension.rs | 160 ++++++++++++++++++ 13 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 extensions/test-extension/Cargo.toml create mode 120000 extensions/test-extension/LICENSE-APACHE create mode 100644 extensions/test-extension/README.md create mode 100644 extensions/test-extension/extension.toml create mode 100644 extensions/test-extension/languages/gleam/config.toml create mode 100644 extensions/test-extension/languages/gleam/highlights.scm create mode 100644 extensions/test-extension/languages/gleam/indents.scm create mode 100644 extensions/test-extension/languages/gleam/outline.scm create mode 100644 extensions/test-extension/src/test_extension.rs diff --git a/Cargo.lock b/Cargo.lock index 14c6edd672d674..3a5b6761027584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13896,6 +13896,13 @@ dependencies = [ "zed_extension_api 0.0.6", ] +[[package]] +name = "zed_test_extension" +version = "0.1.0" +dependencies = [ + "zed_extension_api 0.0.6", +] + [[package]] name = "zed_toml" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index c9fe41f77284f0..5dfcb9d1e2f778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,6 +140,7 @@ members = [ "extensions/snippets", "extensions/svelte", "extensions/terraform", + "extensions/test-extension", "extensions/toml", "extensions/uiua", "extensions/vue", diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index d956d88d64b9d3..f2337aa6b9aee4 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -446,7 +446,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { +async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); @@ -456,7 +456,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { .parent() .unwrap(); let cache_dir = root_dir.join("target"); - let gleam_extension_dir = root_dir.join("extensions").join("gleam"); + let test_extension_id = "test-extension"; + let test_extension_dir = root_dir.join("extensions").join(test_extension_id); let fs = Arc::new(RealFs::default()); let extensions_dir = temp_tree(json!({ @@ -596,7 +597,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { extension_store .update(cx, |store, cx| { - store.install_dev_extension(gleam_extension_dir.clone(), cx) + store.install_dev_extension(test_extension_dir.clone(), cx) }) .await .unwrap(); @@ -611,7 +612,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { .unwrap(); let fake_server = fake_servers.next().await.unwrap(); - let expected_server_path = extensions_dir.join("work/gleam/gleam-v1.2.3/gleam"); + let expected_server_path = + extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam")); let expected_binary_contents = language_server_version.lock().binary_contents.clone(); assert_eq!(fake_server.binary.path, expected_server_path); @@ -725,7 +727,8 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) { // The extension re-fetches the latest version of the language server. let fake_server = fake_servers.next().await.unwrap(); - let new_expected_server_path = extensions_dir.join("work/gleam/gleam-v2.0.0/gleam"); + let new_expected_server_path = + extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam")); let expected_binary_contents = language_server_version.lock().binary_contents.clone(); assert_eq!(fake_server.binary.path, new_expected_server_path); assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); diff --git a/extensions/gleam/extension.toml b/extensions/gleam/extension.toml index 6d76de29675add..089bdea4e6be16 100644 --- a/extensions/gleam/extension.toml +++ b/extensions/gleam/extension.toml @@ -24,4 +24,4 @@ description = "Returns Gleam docs." requires_argument = true tooltip_text = "Insert Gleam docs" -# [indexed_docs_providers.gleam-hexdocs] +[indexed_docs_providers.gleam-hexdocs] diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml new file mode 100644 index 00000000000000..02fa351c28cf9a --- /dev/null +++ b/extensions/test-extension/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_test_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/test_extension.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.0.6" diff --git a/extensions/test-extension/LICENSE-APACHE b/extensions/test-extension/LICENSE-APACHE new file mode 120000 index 00000000000000..1cd601d0a3affa --- /dev/null +++ b/extensions/test-extension/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md new file mode 100644 index 00000000000000..5941f23ec46716 --- /dev/null +++ b/extensions/test-extension/README.md @@ -0,0 +1,5 @@ +# Test Extension + +This is a test extension that we use in the tests for the `extension` crate. + +Originally based off the Gleam extension. diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml new file mode 100644 index 00000000000000..6ac0a3873166e0 --- /dev/null +++ b/extensions/test-extension/extension.toml @@ -0,0 +1,15 @@ +id = "test-extension" +name = "Test Extension" +description = "An extension for use in tests." +version = "0.1.0" +schema_version = 1 +authors = ["Marshall Bowers "] +repository = "https://github.com/zed-industries/zed" + +[language_servers.gleam] +name = "Gleam LSP" +language = "Gleam" + +[grammars.gleam] +repository = "https://github.com/gleam-lang/tree-sitter-gleam" +commit = "8432ffe32ccd360534837256747beb5b1c82fca1" diff --git a/extensions/test-extension/languages/gleam/config.toml b/extensions/test-extension/languages/gleam/config.toml new file mode 100644 index 00000000000000..51874945e2de6b --- /dev/null +++ b/extensions/test-extension/languages/gleam/config.toml @@ -0,0 +1,12 @@ +name = "Gleam" +grammar = "gleam" +path_suffixes = ["gleam"] +line_comments = ["// ", "/// "] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, +] +tab_size = 2 diff --git a/extensions/test-extension/languages/gleam/highlights.scm b/extensions/test-extension/languages/gleam/highlights.scm new file mode 100644 index 00000000000000..4b85b88d0151a1 --- /dev/null +++ b/extensions/test-extension/languages/gleam/highlights.scm @@ -0,0 +1,130 @@ +; Comments +(module_comment) @comment +(statement_comment) @comment +(comment) @comment + +; Constants +(constant + name: (identifier) @constant) + +; Variables +(identifier) @variable +(discard) @comment.unused + +; Modules +(module) @module +(import alias: (identifier) @module) +(remote_type_identifier + module: (identifier) @module) +(remote_constructor_name + module: (identifier) @module) +((field_access + record: (identifier) @module + field: (label) @function) + (#is-not? local)) + +; Functions +(unqualified_import (identifier) @function) +(unqualified_import "type" (type_identifier) @type) +(unqualified_import (type_identifier) @constructor) +(function + name: (identifier) @function) +(external_function + name: (identifier) @function) +(function_parameter + name: (identifier) @variable.parameter) +((function_call + function: (identifier) @function) + (#is-not? local)) +((binary_expression + operator: "|>" + right: (identifier) @function) + (#is-not? local)) + +; "Properties" +; Assumed to be intended to refer to a name for a field; something that comes +; before ":" or after "." +; e.g. record field names, tuple indices, names for named arguments, etc +(label) @property +(tuple_access + index: (integer) @property) + +; Attributes +(attribute + "@" @attribute + name: (identifier) @attribute) + +(attribute_value (identifier) @constant) + +; Type names +(remote_type_identifier) @type +(type_identifier) @type + +; Data constructors +(constructor_name) @constructor + +; Literals +(string) @string +((escape_sequence) @warning + ; Deprecated in v0.33.0-rc2: + (#eq? @warning "\\e")) +(escape_sequence) @string.escape +(bit_string_segment_option) @function.builtin +(integer) @number +(float) @number + +; Reserved identifiers +; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, +; refactor this to use `#any-of?` rather than `#match?` +((identifier) @warning + (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) + +; Keywords +[ + (visibility_modifier) ; "pub" + (opacity_modifier) ; "opaque" + "as" + "assert" + "case" + "const" + ; DEPRECATED: 'external' was removed in v0.30. + "external" + "fn" + "if" + "import" + "let" + "panic" + "todo" + "type" + "use" +] @keyword + +; Operators +(binary_expression + operator: _ @operator) +(boolean_negation "!" @operator) +(integer_negation "-" @operator) + +; Punctuation +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<<" + ">>" +] @punctuation.bracket +[ + "." + "," + ;; Controversial -- maybe some are operators? + ":" + "#" + "=" + "->" + ".." + "-" + "<-" +] @punctuation.delimiter diff --git a/extensions/test-extension/languages/gleam/indents.scm b/extensions/test-extension/languages/gleam/indents.scm new file mode 100644 index 00000000000000..112b414aa45f27 --- /dev/null +++ b/extensions/test-extension/languages/gleam/indents.scm @@ -0,0 +1,3 @@ +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/extensions/test-extension/languages/gleam/outline.scm b/extensions/test-extension/languages/gleam/outline.scm new file mode 100644 index 00000000000000..5df7a6af800e8e --- /dev/null +++ b/extensions/test-extension/languages/gleam/outline.scm @@ -0,0 +1,31 @@ +(external_type + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(type_definition + (visibility_modifier)? @context + (opacity_modifier)? @context + "type" @context + (type_name) @name) @item + +(data_constructor + (constructor_name) @name) @item + +(data_constructor_argument + (label) @name) @item + +(type_alias + (visibility_modifier)? @context + "type" @context + (type_name) @name) @item + +(function + (visibility_modifier)? @context + "fn" @context + name: (_) @name) @item + +(constant + (visibility_modifier)? @context + "const" @context + name: (_) @name) @item diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs new file mode 100644 index 00000000000000..bbdb5877823140 --- /dev/null +++ b/extensions/test-extension/src/test_extension.rs @@ -0,0 +1,160 @@ +use std::fs; +use zed::lsp::CompletionKind; +use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; +use zed_extension_api::{self as zed, Result}; + +struct TestExtension { + cached_binary_path: Option, +} + +impl TestExtension { + 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( + "gleam-lang/gleam", + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + + let (platform, arch) = zed::current_platform(); + let asset_name = format!( + "gleam-{version}-{arch}-{os}.tar.gz", + version = release.version, + arch = match arch { + zed::Architecture::Aarch64 => "aarch64", + zed::Architecture::X86 => "x86", + zed::Architecture::X8664 => "x86_64", + }, + os = match platform { + zed::Os::Mac => "apple-darwin", + zed::Os::Linux => "unknown-linux-musl", + 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!("gleam-{}", release.version); + let binary_path = format!("{version_dir}/gleam"); + + 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::GzipTar, + ) + .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 TestExtension { + 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 label_for_completion( + &self, + _language_server_id: &LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + let name = &completion.label; + let ty = strip_newlines_from_detail(&completion.detail?); + let let_binding = "let a"; + let colon = ": "; + let assignment = " = "; + let call = match completion.kind? { + CompletionKind::Function | CompletionKind::Constructor => "()", + _ => "", + }; + let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); + start..start + name.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len(); + start..start + colon.len() + }), + CodeLabelSpan::code_range({ + let start = let_binding.len() + colon.len(); + start..start + ty.len() + }), + ], + filter_range: (0..name.len()).into(), + code, + }) + } +} + +zed::register_extension!(TestExtension); + +/// Removes newlines from the completion detail. +/// +/// The Gleam LSP can return types containing newlines, which causes formatting +/// issues within the Zed completions menu. +fn strip_newlines_from_detail(detail: &str) -> String { + let without_newlines = detail + .replace("->\n ", "-> ") + .replace("\n ", "") + .replace(",\n", ""); + + let comma_delimited_parts = without_newlines.split(','); + comma_delimited_parts + .map(|part| part.trim()) + .collect::>() + .join(", ") +} From 48763d0663c2a6ae3d2f368b5c6d4218fb2e4d14 Mon Sep 17 00:00:00 2001 From: Xiaoguang Wang Date: Wed, 3 Jul 2024 23:23:52 +0800 Subject: [PATCH 09/40] vim: Add vim bindings for outline panel (#13763) Release Notes: - vim: Add vim bindings for outline panel #13763 --- assets/keymaps/vim.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 498fd018fe59e5..7a0b2646d651d2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -722,5 +722,14 @@ "g g": "menu::SelectFirst", "-": "project_panel::SelectParent" } + }, + { + "context": "OutlinePanel", + "bindings": { + "j": "menu::SelectNext", + "k": "menu::SelectPrev", + "shift-g": "menu::SelectLast", + "g g": "menu::SelectFirst" + } } ] From 38fb841d1f4f8d18bdb097a4805398c59759b38a Mon Sep 17 00:00:00 2001 From: Connor Finnell Date: Wed, 3 Jul 2024 09:53:19 -0600 Subject: [PATCH 10/40] Use regex to properly select Go test runnable (#13750) This is already done when selecting a subtest; by wrapping the test name with `^{}$` the runnable will avoid selecting additional tests with the same prefix. Without this fix, selecting the runnable for `TestExample` will also run `TestExample2`. Release Notes: - Fixed Golang tasks spawning tests starting with the current function name and not using the exact match. --- crates/languages/src/go.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 48641c5729e1c3..47226efefa606f 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -518,7 +518,7 @@ impl ContextProvider for GoContextProvider { "test".into(), GO_PACKAGE_TASK_VARIABLE.template_value(), "-run".into(), - VariableName::Symbol.template_value(), + format!("^{}$", VariableName::Symbol.template_value(),), ], tags: vec!["go-test".to_owned()], ..TaskTemplate::default() From cceebee397e54b1cdc7ab9c545fbac700bf45749 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 3 Jul 2024 12:15:16 -0400 Subject: [PATCH 11/40] v0.144.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 3a5b6761027584..87df412855b2c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13607,7 +13607,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.143.0" +version = "0.144.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a910d41a463d32..cd1a952c52cd36 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.143.0" +version = "0.144.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] From 22a9293cba76e5e1ab4afa5f73a5d0e578198e4a Mon Sep 17 00:00:00 2001 From: Max McKenzie Date: Thu, 4 Jul 2024 01:22:32 +0900 Subject: [PATCH 12/40] docs: Document setting up Claude in the Assistant (#13765) Release Notes: - Added documentation on how to set up Claude as the assistant. --------- Co-authored-by: Peter Tripp Co-authored-by: Gilles Peiffer Co-authored-by: Peter Tripp --- docs/src/assistant-panel.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/src/assistant-panel.md b/docs/src/assistant-panel.md index 444b851f92b59c..e4d52dc97bab70 100644 --- a/docs/src/assistant-panel.md +++ b/docs/src/assistant-panel.md @@ -134,6 +134,25 @@ You can use Ollama with the Zed assistant by making Ollama appear as an OpenAPI ``` 5. Restart Zed +## Using Claude 3.5 Sonnet + +You can use Claude with the Zed assistant by adding the following settings: + +```json +"assistant": { + "version": "1", + "provider": { + "default_model": "claude-3-5-sonnet", + "name": "anthropic" + } +}, +``` + +When you save the settings, the assistant panel will open and ask you to add your Anthropic API key. +You need can obtain this key [here](https://console.anthropic.com/settings/keys). + +Even if you pay for Claude Pro, you will still have to [pay for additional credits](https://console.anthropic.com/settings/plans) to use it via the API. + ## Prompt Library **Warning: This feature is experimental and the format of prompts is _highly_ likely to change. Use at your own risk!** From 2f05f5bc5c80744e54af36127d17f4291e2ced5e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 3 Jul 2024 16:25:25 +0000 Subject: [PATCH 13/40] Make initial settings valid JSON (#13785) --- assets/settings/initial_user_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 7dd03c278157b0..2c0c5849ca0499 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -12,6 +12,6 @@ "theme": { "mode": "system", "light": "One Light", - "dark": "One Dark", - }, + "dark": "One Dark" + } } From f024fcff3d1eeed5729463222d452a6d7e9fe59a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jul 2024 10:31:14 -0600 Subject: [PATCH 14/40] Linux builds on stable (#13744) Release Notes: - First beta version of Linux --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04c1b6999a30cb..d5ea962e1df529 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,7 +402,7 @@ jobs: - name: Upload app bundle to release uses: softprops/action-gh-release@v1 - if: ${{ env.RELEASE_CHANNEL == 'preview' }} + if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} with: draft: true prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} From 98699a65c13d5cb3164e82a2be8892e38898e25b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 12:57:08 -0400 Subject: [PATCH 15/40] gleam: Improve indexing of HexDocs (#13787) This PR improves the indexing of HexDocs content for Gleam packages. We now index each of the modules in the package instead of just the root. Release Notes: - N/A --- extensions/gleam/src/gleam.rs | 43 +------ extensions/gleam/src/hexdocs.rs | 205 ++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 37 deletions(-) create mode 100644 extensions/gleam/src/hexdocs.rs diff --git a/extensions/gleam/src/gleam.rs b/extensions/gleam/src/gleam.rs index 2907bcf6ada44b..27478a2915f0a7 100644 --- a/extensions/gleam/src/gleam.rs +++ b/extensions/gleam/src/gleam.rs @@ -1,7 +1,6 @@ -use html_to_markdown::{convert_html_to_markdown, TagHandler}; -use std::cell::RefCell; +mod hexdocs; + use std::fs; -use std::rc::Rc; use zed::lsp::CompletionKind; use zed::{ CodeLabel, CodeLabelSpan, HttpRequest, KeyValueStore, LanguageServerId, SlashCommand, @@ -9,6 +8,8 @@ use zed::{ }; use zed_extension_api::{self as zed, Result}; +use crate::hexdocs::convert_hexdocs_to_markdown; + struct GleamExtension { cached_binary_path: Option, } @@ -191,19 +192,7 @@ impl zed::Extension for GleamExtension { ), })?; - let mut handlers: Vec = vec![ - Rc::new(RefCell::new( - html_to_markdown::markdown::WebpageChromeRemover, - )), - Rc::new(RefCell::new(html_to_markdown::markdown::ParagraphHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::HeadingHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::ListHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::TableHandler::new())), - Rc::new(RefCell::new(html_to_markdown::markdown::StyledTextHandler)), - ]; - - let markdown = convert_html_to_markdown(response.body.as_bytes(), &mut handlers) - .map_err(|err| format!("failed to convert docs to Markdown {err}"))?; + let (markdown, _modules) = convert_hexdocs_to_markdown(response.body.as_bytes())?; let mut text = String::new(); text.push_str(&markdown); @@ -244,27 +233,7 @@ impl zed::Extension for GleamExtension { database: &KeyValueStore, ) -> Result<(), String> { match provider.as_str() { - "gleam-hexdocs" => { - let response = zed::fetch(&HttpRequest { - url: format!("https://hexdocs.pm/{package}"), - })?; - - let mut handlers: Vec = vec![ - Rc::new(RefCell::new( - html_to_markdown::markdown::WebpageChromeRemover, - )), - Rc::new(RefCell::new(html_to_markdown::markdown::ParagraphHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::HeadingHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::ListHandler)), - Rc::new(RefCell::new(html_to_markdown::markdown::TableHandler::new())), - Rc::new(RefCell::new(html_to_markdown::markdown::StyledTextHandler)), - ]; - - let markdown = convert_html_to_markdown(response.body.as_bytes(), &mut handlers) - .map_err(|err| format!("failed to convert docs to Markdown {err}"))?; - - Ok(database.insert(&package, &markdown)?) - } + "gleam-hexdocs" => hexdocs::index(package, database), _ => Ok(()), } } diff --git a/extensions/gleam/src/hexdocs.rs b/extensions/gleam/src/hexdocs.rs new file mode 100644 index 00000000000000..cc21746934cc14 --- /dev/null +++ b/extensions/gleam/src/hexdocs.rs @@ -0,0 +1,205 @@ +use std::cell::RefCell; +use std::collections::BTreeSet; +use std::io::Read; +use std::rc::Rc; + +use html_to_markdown::markdown::{ + HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler, +}; +use html_to_markdown::{ + convert_html_to_markdown, HandleTag, HandlerOutcome, HtmlElement, MarkdownWriter, + StartTagOutcome, TagHandler, +}; +use zed_extension_api::{self as zed, HttpRequest, KeyValueStore, Result}; + +pub fn index(package: String, database: &KeyValueStore) -> Result<()> { + let response = zed::fetch(&HttpRequest { + url: format!("https://hexdocs.pm/{package}"), + })?; + + let (package_root_markdown, modules) = convert_hexdocs_to_markdown(response.body.as_bytes())?; + + database.insert(&package, &package_root_markdown)?; + + for module in modules { + let response = zed::fetch(&HttpRequest { + url: format!("https://hexdocs.pm/{package}/{module}.html"), + })?; + + let (markdown, _modules) = convert_hexdocs_to_markdown(response.body.as_bytes())?; + + database.insert(&module, &markdown)?; + } + + Ok(()) +} + +pub fn convert_hexdocs_to_markdown(html: impl Read) -> Result<(String, Vec)> { + let module_collector = Rc::new(RefCell::new(GleamModuleCollector::new())); + + let mut handlers: Vec = vec![ + module_collector.clone(), + Rc::new(RefCell::new(GleamChromeRemover)), + Rc::new(RefCell::new(NavSkipper::new(ParagraphHandler))), + Rc::new(RefCell::new(NavSkipper::new(HeadingHandler))), + Rc::new(RefCell::new(NavSkipper::new(ListHandler))), + Rc::new(RefCell::new(NavSkipper::new(TableHandler::new()))), + Rc::new(RefCell::new(NavSkipper::new(StyledTextHandler))), + ]; + + let markdown = convert_html_to_markdown(html, &mut handlers) + .map_err(|err| format!("failed to convert docs to Markdown {err}"))?; + + let modules = module_collector + .borrow() + .modules + .iter() + .cloned() + .collect::>(); + + Ok((markdown, modules)) +} + +/// A higher-order handler that skips all content from the `nav`. +/// +/// We still need to traverse the `nav` for collecting information, but +/// we don't want to include any of its content in the resulting Markdown. +pub struct NavSkipper { + handler: T, +} + +impl NavSkipper { + pub fn new(handler: T) -> Self { + Self { handler } + } +} + +impl HandleTag for NavSkipper { + fn should_handle(&self, tag: &str) -> bool { + tag == "nav" || self.handler.should_handle(tag) + } + + fn handle_tag_start( + &mut self, + tag: &HtmlElement, + writer: &mut MarkdownWriter, + ) -> StartTagOutcome { + if writer.is_inside("nav") { + return StartTagOutcome::Continue; + } + + self.handler.handle_tag_start(tag, writer) + } + + fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { + if writer.is_inside("nav") { + return; + } + + self.handler.handle_tag_end(tag, writer) + } + + fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { + if writer.is_inside("nav") { + return HandlerOutcome::Handled; + } + + self.handler.handle_text(text, writer) + } +} + +pub struct GleamChromeRemover; + +impl HandleTag for GleamChromeRemover { + fn should_handle(&self, tag: &str) -> bool { + match tag { + "head" | "script" | "style" | "svg" | "header" | "footer" | "a" => true, + _ => false, + } + } + + fn handle_tag_start( + &mut self, + tag: &HtmlElement, + _writer: &mut MarkdownWriter, + ) -> StartTagOutcome { + match tag.tag() { + "head" | "script" | "style" | "svg" | "header" | "footer" => { + return StartTagOutcome::Skip; + } + "a" => { + if tag.attr("onclick").is_some() { + return StartTagOutcome::Skip; + } + } + _ => {} + } + + StartTagOutcome::Continue + } +} + +pub struct GleamModuleCollector { + modules: BTreeSet, + has_seen_modules_header: bool, +} + +impl GleamModuleCollector { + pub fn new() -> Self { + Self { + modules: BTreeSet::new(), + has_seen_modules_header: false, + } + } + + fn parse_module(tag: &HtmlElement) -> Option { + if tag.tag() != "a" { + return None; + } + + let href = tag.attr("href")?; + if href.starts_with('#') || href.starts_with("https://") || href.starts_with("../") { + return None; + } + + let module_name = href.trim_start_matches("./").trim_end_matches(".html"); + + Some(module_name.to_owned()) + } +} + +impl HandleTag for GleamModuleCollector { + fn should_handle(&self, tag: &str) -> bool { + match tag { + "h2" | "a" => true, + _ => false, + } + } + + fn handle_tag_start( + &mut self, + tag: &HtmlElement, + writer: &mut MarkdownWriter, + ) -> StartTagOutcome { + match tag.tag() { + "a" => { + if self.has_seen_modules_header && writer.is_inside("li") { + if let Some(module_name) = Self::parse_module(tag) { + self.modules.insert(module_name); + } + } + } + _ => {} + } + + StartTagOutcome::Continue + } + + fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { + if writer.is_inside("nav") && writer.is_inside("h2") && text == "Modules" { + self.has_seen_modules_header = true; + } + + HandlerOutcome::NoOp + } +} From 47aa761ca9dee79852056897090480ff7f5ba7a9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 Jul 2024 11:28:09 -0700 Subject: [PATCH 16/40] Linux window decorations (#13611) This PR adds support for full client side decorations on X11 and Wayland TODO: - [x] Adjust GPUI APIs to expose CSD related information - [x] Implement remaining CSD features (Resizing, window border, window shadow) - [x] Integrate with existing background appearance and window transparency - [x] Figure out how to check if the window is tiled on X11 - [x] Implement in Zed - [x] Repeatedly maximizing and unmaximizing can panic - [x] Resizing is strangely slow - [x] X11 resizing and movement doesn't work for this: https://discord.com/channels/869392257814519848/1204679850208657418/1256816908519604305 - [x] The top corner can clip with current styling - [x] Pressing titlebar buttons doesn't work - [x] Not showing maximize / unmaximize buttons - [x] Noisy transparency logs / surface transparency problem https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030 - [x] Strange offsets when dragging the project panel https://github.com/zed-industries/zed/pull/13611#pullrequestreview-2154606261 - [x] Shadow inset with `_GTK_FRAME_EXTENTS` doesn't respect tiling on X11 (observe by snapping an X11 window in any direction) Release Notes: - N/A --------- Co-authored-by: conrad Co-authored-by: Owen Law <81528246+someone13574@users.noreply.github.com> Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com> Co-authored-by: Conrad Irwin --- crates/collab_ui/src/collab_ui.rs | 5 +- crates/gpui/Cargo.toml | 5 + crates/gpui/examples/hello_world.rs | 2 +- crates/gpui/examples/window_positioning.rs | 1 + crates/gpui/examples/window_shadow.rs | 222 +++++++ crates/gpui/src/color.rs | 10 + crates/gpui/src/geometry.rs | 36 +- crates/gpui/src/platform.rs | 124 +++- crates/gpui/src/platform/linux/platform.rs | 4 + .../gpui/src/platform/linux/wayland/client.rs | 2 +- .../gpui/src/platform/linux/wayland/window.rs | 425 ++++++++++--- crates/gpui/src/platform/linux/x11/client.rs | 12 +- crates/gpui/src/platform/linux/x11/window.rs | 397 +++++++++--- crates/gpui/src/platform/mac/platform.rs | 16 +- crates/gpui/src/platform/mac/window.rs | 15 +- crates/gpui/src/platform/test/window.rs | 10 +- crates/gpui/src/platform/windows/window.rs | 20 +- crates/gpui/src/window.rs | 68 ++- crates/theme/src/theme.rs | 6 +- crates/title_bar/src/platforms.rs | 1 - .../src/platforms/platform_generic.rs | 47 -- .../title_bar/src/platforms/platform_linux.rs | 29 +- crates/title_bar/src/title_bar.rs | 68 ++- crates/title_bar/src/window_controls.rs | 6 +- crates/workspace/src/status_bar.rs | 16 +- crates/workspace/src/workspace.rs | 568 +++++++++++++----- crates/zed/src/zed.rs | 1 + crates/zed/src/zed/linux_prompts.rs | 53 +- 28 files changed, 1631 insertions(+), 538 deletions(-) create mode 100644 crates/gpui/examples/window_shadow.rs delete mode 100644 crates/title_bar/src/platforms/platform_generic.rs diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index fe177603cb0236..8b704b6a05fc7e 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -10,7 +10,7 @@ use std::{rc::Rc, sync::Arc}; pub use collab_panel::CollabPanel; use gpui::{ point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds, - WindowKind, WindowOptions, + WindowDecorations, WindowKind, WindowOptions, }; use panel_settings::MessageEditorSettings; pub use panel_settings::{ @@ -63,8 +63,9 @@ fn notification_window_options( kind: WindowKind::PopUp, is_movable: false, display_id: Some(screen.id()), - window_background: WindowBackgroundAppearance::default(), + window_background: WindowBackgroundAppearance::Transparent, app_id: Some(app_id.to_owned()), window_min_size: None, + window_decorations: Some(WindowDecorations::Client), } } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index c7c3a92a953997..04f62f28e71118 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -133,6 +133,7 @@ x11rb = { version = "0.13.0", features = [ "xinput", "cursor", "resource_manager", + "sync", ] } xkbcommon = { version = "0.7", features = ["wayland", "x11"] } xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [ @@ -160,6 +161,10 @@ path = "examples/image/image.rs" name = "set_menus" path = "examples/set_menus.rs" +[[example]] +name = "window_shadow" +path = "examples/window_shadow.rs" + [[example]] name = "input" path = "examples/input.rs" diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index 96ab335b08466b..961212fa62a265 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -23,7 +23,7 @@ impl Render for HelloWorld { fn main() { App::new().run(|cx: &mut AppContext| { - let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), diff --git a/crates/gpui/examples/window_positioning.rs b/crates/gpui/examples/window_positioning.rs index b22dc589746c4c..0c5f216c2e9bda 100644 --- a/crates/gpui/examples/window_positioning.rs +++ b/crates/gpui/examples/window_positioning.rs @@ -52,6 +52,7 @@ fn main() { is_movable: false, app_id: None, window_min_size: None, + window_decorations: None, } }; diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs new file mode 100644 index 00000000000000..122231f6b5a8a0 --- /dev/null +++ b/crates/gpui/examples/window_shadow.rs @@ -0,0 +1,222 @@ +use gpui::*; +use prelude::FluentBuilder; + +struct WindowShadow {} + +/* +Things to do: +1. We need a way of calculating which edge or corner the mouse is on, + and then dispatch on that +2. We need to improve the shadow rendering significantly +3. We need to implement the techniques in here in Zed +*/ + +impl Render for WindowShadow { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let decorations = cx.window_decorations(); + let rounding = px(10.0); + let shadow_size = px(10.0); + let border_size = px(1.0); + let grey = rgb(0x808080); + cx.set_client_inset(shadow_size); + + div() + .id("window-backdrop") + .bg(transparent_black()) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling, .. } => div + .bg(gpui::transparent_black()) + .child( + canvas( + |_bounds, cx| { + cx.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + cx.window_bounds().get_bounds().size, + ), + false, + ) + }, + move |_bounds, hitbox, cx| { + let mouse = cx.mouse_position(); + let size = cx.window_bounds().get_bounds().size; + let Some(edge) = resize_edge(mouse, shadow_size, size) else { + return; + }; + cx.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => { + CursorStyle::ResizeUpDown + } + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(rounding) + }) + .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) + .when(!tiling.top, |div| div.pt(shadow_size)) + .when(!tiling.bottom, |div| div.pb(shadow_size)) + .when(!tiling.left, |div| div.pl(shadow_size)) + .when(!tiling.right, |div| div.pr(shadow_size)) + .on_mouse_move(|_e, cx| cx.refresh()) + .on_mouse_down(MouseButton::Left, move |e, cx| { + let size = cx.window_bounds().get_bounds().size; + let pos = e.position; + + match resize_edge(pos, shadow_size, size) { + Some(edge) => cx.start_window_resize(edge), + None => cx.start_window_move(), + }; + }), + }) + .size_full() + .child( + div() + .cursor(CursorStyle::Arrow) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .border_color(grey) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(rounding) + }) + .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) + .when(!tiling.top, |div| div.border_t(border_size)) + .when(!tiling.bottom, |div| div.border_b(border_size)) + .when(!tiling.left, |div| div.border_l(border_size)) + .when(!tiling.right, |div| div.border_r(border_size)) + .when(!tiling.is_tiled(), |div| { + div.shadow(smallvec::smallvec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + blur_radius: shadow_size / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + }]) + }), + }) + .on_mouse_move(|_e, cx| { + cx.stop_propagation(); + }) + .bg(gpui::rgb(0xCCCCFF)) + .size_full() + .flex() + .flex_col() + .justify_around() + .child( + div().w_full().flex().flex_row().justify_around().child( + div() + .flex() + .bg(white()) + .size(Length::Definite(Pixels(300.0).into())) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child( + div() + .id("hello") + .w(px(200.0)) + .h(px(100.0)) + .bg(green()) + .shadow(smallvec::smallvec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 1.0, + }, + blur_radius: px(20.0), + spread_radius: px(0.0), + offset: point(px(0.0), px(0.0)), + }]) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { .. } => div + .on_mouse_down(MouseButton::Left, |_e, cx| { + cx.start_window_move(); + }) + .on_click(|e, cx| { + if e.down.button == MouseButton::Right { + cx.show_window_menu(e.up.position); + } + }) + .text_color(black()) + .child("this is the custom titlebar"), + }), + ), + ), + ), + ) + } +} + +fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { + let edge = if pos.y < shadow_size && pos.x < shadow_size { + ResizeEdge::TopLeft + } else if pos.y < shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::TopRight + } else if pos.y < shadow_size { + ResizeEdge::Top + } else if pos.y > size.height - shadow_size && pos.x < shadow_size { + ResizeEdge::BottomLeft + } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::BottomRight + } else if pos.y > size.height - shadow_size { + ResizeEdge::Bottom + } else if pos.x < shadow_size { + ResizeEdge::Left + } else if pos.x > size.width - shadow_size { + ResizeEdge::Right + } else { + return None; + }; + Some(edge) +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_background: WindowBackgroundAppearance::Opaque, + window_decorations: Some(WindowDecorations::Client), + ..Default::default() + }, + |cx| { + cx.new_view(|cx| { + cx.observe_window_appearance(|_, cx| { + cx.refresh(); + }) + .detach(); + WindowShadow {} + }) + }, + ) + .unwrap(); + }); +} diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 2cf2ad55f2a1bb..585255b450377e 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -309,6 +309,16 @@ pub fn transparent_black() -> Hsla { } } +/// Transparent black in [`Hsla`] +pub fn transparent_white() -> Hsla { + Hsla { + h: 0., + s: 0., + l: 1., + a: 0., + } +} + /// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1] pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { Hsla { diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index e6cde4fce2e00f..f3cd933e2b67c6 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -883,6 +883,14 @@ where self.size.height = self.size.height.clone() + double_amount; } + /// inset the bounds by a specified amount + /// Note that this may panic if T does not support negative values + pub fn inset(&self, amount: T) -> Self { + let mut result = self.clone(); + result.dilate(T::default() - amount); + result + } + /// Returns the center point of the bounds. /// /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height @@ -1266,12 +1274,36 @@ where /// size: Size { width: 10.0, height: 20.0 }, /// }); /// ``` - pub fn map_origin(self, f: impl Fn(Point) -> Point) -> Bounds { + pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds { Bounds { - origin: f(self.origin), + origin: self.origin.map(f), size: self.size, } } + + /// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map_size(|value| value * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); + /// ``` + pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds { + Bounds { + origin: self.origin, + size: self.size.map(f), + } + } } /// Checks if the bounds represent an empty area. diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 238ccb87d1cd84..3d81e88fb4cfb3 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -210,6 +210,83 @@ impl Debug for DisplayId { unsafe impl Send for DisplayId {} +/// Which part of the window to resize +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeEdge { + /// The top edge + Top, + /// The top right corner + TopRight, + /// The right edge + Right, + /// The bottom right corner + BottomRight, + /// The bottom edge + Bottom, + /// The bottom left corner + BottomLeft, + /// The left edge + Left, + /// The top left corner + TopLeft, +} + +/// A type to describe the appearance of a window +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum WindowDecorations { + #[default] + /// Server side decorations + Server, + /// Client side decorations + Client, +} + +/// A type to describe how this window is currently configured +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum Decorations { + /// The window is configured to use server side decorations + #[default] + Server, + /// The window is configured to use client side decorations + Client { + /// The edge tiling state + tiling: Tiling, + }, +} + +/// What window controls this platform supports +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub struct WindowControls { + /// Whether this platform supports fullscreen + pub fullscreen: bool, + /// Whether this platform supports maximize + pub maximize: bool, + /// Whether this platform supports minimize + pub minimize: bool, + /// Whether this platform supports a window menu + pub window_menu: bool, +} + +/// A type to describe which sides of the window are currently tiled in some way +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub struct Tiling { + /// Whether the top edge is tiled + pub top: bool, + /// Whether the left edge is tiled + pub left: bool, + /// Whether the right edge is tiled + pub right: bool, + /// Whether the bottom edge is tiled + pub bottom: bool, +} + +impl Tiling { + /// Whether any edge is tiled + pub fn is_tiled(&self) -> bool { + self.top || self.left || self.right || self.bottom + } +} + pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn bounds(&self) -> Bounds; fn is_maximized(&self) -> bool; @@ -232,10 +309,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn activate(&self); fn is_active(&self) -> bool; fn set_title(&mut self, title: &str); - fn set_app_id(&mut self, app_id: &str); - fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance); - fn set_edited(&mut self, edited: bool); - fn show_character_palette(&self); + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance); fn minimize(&self); fn zoom(&self); fn toggle_fullscreen(&self); @@ -252,12 +326,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; + // macOS specific methods + fn set_edited(&mut self, _edited: bool) {} + fn show_character_palette(&self) {} + #[cfg(target_os = "windows")] fn get_raw_handle(&self) -> windows::HWND; - fn show_window_menu(&self, position: Point); - fn start_system_move(&self); - fn should_render_window_controls(&self) -> bool; + // Linux specific methods + fn request_decorations(&self, _decorations: WindowDecorations) {} + fn show_window_menu(&self, _position: Point) {} + fn start_window_move(&self) {} + fn start_window_resize(&self, _edge: ResizeEdge) {} + fn window_decorations(&self) -> Decorations { + Decorations::Server + } + fn set_app_id(&mut self, _app_id: &str) {} + fn window_controls(&self) -> WindowControls { + WindowControls { + fullscreen: true, + maximize: true, + minimize: true, + window_menu: false, + } + } + fn set_client_inset(&self, _inset: Pixels) {} #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { @@ -570,6 +663,10 @@ pub struct WindowOptions { /// Window minimum size pub window_min_size: Option>, + + /// Whether to use client or server side decorations. Wayland only + /// Note that this may be ignored. + pub window_decorations: Option, } /// The variables that can be configured when creating a new window @@ -596,8 +693,6 @@ pub(crate) struct WindowParams { pub display_id: Option, - pub window_background: WindowBackgroundAppearance, - #[cfg_attr(target_os = "linux", allow(dead_code))] pub window_min_size: Option>, } @@ -649,6 +744,7 @@ impl Default for WindowOptions { window_background: WindowBackgroundAppearance::default(), app_id: None, window_min_size: None, + window_decorations: None, } } } @@ -659,7 +755,7 @@ pub struct TitlebarOptions { /// The initial title of the window pub title: Option, - /// Whether the titlebar should appear transparent + /// Whether the titlebar should appear transparent (macOS only) pub appears_transparent: bool, /// The position of the macOS traffic light buttons @@ -805,6 +901,14 @@ pub enum CursorStyle { /// corresponds to the CSS cursor value `ns-resize` ResizeUpDown, + /// A resize cursor directing up-left and down-right + /// corresponds to the CSS cursor value `nesw-resize` + ResizeUpLeftDownRight, + + /// A resize cursor directing up-right and down-left + /// corresponds to the CSS cursor value `nwse-resize` + ResizeUpRightDownLeft, + /// A cursor indicating that the item/column can be resized horizontally. /// corresponds to the CSS curosr value `col-resize` ResizeColumn, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 54fc4aa17d59b0..9eef459d5dc710 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -572,6 +572,8 @@ impl CursorStyle { CursorStyle::ResizeUp => Shape::NResize, CursorStyle::ResizeDown => Shape::SResize, CursorStyle::ResizeUpDown => Shape::NsResize, + CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize, + CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize, CursorStyle::ResizeColumn => Shape::ColResize, CursorStyle::ResizeRow => Shape::RowResize, CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText, @@ -599,6 +601,8 @@ impl CursorStyle { CursorStyle::ResizeUp => "n-resize", CursorStyle::ResizeDown => "s-resize", CursorStyle::ResizeUpDown => "ns-resize", + CursorStyle::ResizeUpLeftDownRight => "nwse-resize", + CursorStyle::ResizeUpRightDownLeft => "nesw-resize", CursorStyle::ResizeColumn => "col-resize", CursorStyle::ResizeRow => "row-resize", CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 22750d1814d62e..2728a141eef5a2 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -138,7 +138,7 @@ impl Globals { primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(), shm: globals.bind(&qh, 1..=1, ()).unwrap(), seat, - wm_base: globals.bind(&qh, 1..=1, ()).unwrap(), + wm_base: globals.bind(&qh, 2..=5, ()).unwrap(), viewporter: globals.bind(&qh, 1..=1, ()).ok(), fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index e82a5fc8429c10..5ab64e9e1ef86d 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -25,9 +25,10 @@ use crate::platform::linux::wayland::serial::SerialKind; use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow}; use crate::scene::Scene; use crate::{ - px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay, - PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowParams, + px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels, + PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling, + WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControls, WindowDecorations, WindowParams, }; #[derive(Default)] @@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow { } } +#[derive(Debug)] struct InProgressConfigure { size: Option>, fullscreen: bool, maximized: bool, + tiling: Tiling, } pub struct WaylandWindowState { @@ -84,14 +87,20 @@ pub struct WaylandWindowState { bounds: Bounds, scale: f32, input_handler: Option, - decoration_state: WaylandDecorationState, + decorations: WindowDecorations, + background_appearance: WindowBackgroundAppearance, fullscreen: bool, maximized: bool, - windowed_bounds: Bounds, + tiling: Tiling, + window_bounds: Bounds, client: WaylandClientStatePtr, handle: AnyWindowHandle, active: bool, in_progress_configure: Option, + in_progress_window_controls: Option, + window_controls: WindowControls, + inset: Option, + requested_inset: Option, } #[derive(Clone)] @@ -142,7 +151,7 @@ impl WaylandWindowState { height: options.bounds.size.height.0 as u32, depth: 1, }, - transparent: options.window_background != WindowBackgroundAppearance::Opaque, + transparent: true, }; Ok(Self { @@ -160,17 +169,34 @@ impl WaylandWindowState { bounds: options.bounds, scale: 1.0, input_handler: None, - decoration_state: WaylandDecorationState::Client, + decorations: WindowDecorations::Client, + background_appearance: WindowBackgroundAppearance::Opaque, fullscreen: false, maximized: false, - windowed_bounds: options.bounds, + tiling: Tiling::default(), + window_bounds: options.bounds, in_progress_configure: None, client, appearance, handle, active: false, + in_progress_window_controls: None, + // Assume that we can do anything, unless told otherwise + window_controls: WindowControls { + fullscreen: true, + maximize: true, + minimize: true, + window_menu: true, + }, + inset: None, + requested_inset: None, }) } + + pub fn is_transparent(&self) -> bool { + self.decorations == WindowDecorations::Client + || self.background_appearance != WindowBackgroundAppearance::Opaque + } } pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); @@ -235,7 +261,7 @@ impl WaylandWindow { .wm_base .get_xdg_surface(&surface, &globals.qh, surface.id()); let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); - toplevel.set_min_size(200, 200); + toplevel.set_min_size(50, 50); if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); @@ -246,13 +272,7 @@ impl WaylandWindow { .decoration_manager .as_ref() .map(|decoration_manager| { - let decoration = decoration_manager.get_toplevel_decoration( - &toplevel, - &globals.qh, - surface.id(), - ); - decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide); - decoration + decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) }); let viewport = globals @@ -298,7 +318,7 @@ impl WaylandWindowStatePtr { pub fn frame(&self, request_frame_callback: bool) { if request_frame_callback { - let state = self.state.borrow_mut(); + let mut state = self.state.borrow_mut(); state.surface.frame(&state.globals.qh, state.surface.id()); drop(state); } @@ -311,6 +331,18 @@ impl WaylandWindowStatePtr { pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { match event { xdg_surface::Event::Configure { serial } => { + { + let mut state = self.state.borrow_mut(); + if let Some(window_controls) = state.in_progress_window_controls.take() { + state.window_controls = window_controls; + + drop(state); + let mut callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } + } + } { let mut state = self.state.borrow_mut(); @@ -318,18 +350,21 @@ impl WaylandWindowStatePtr { let got_unmaximized = state.maximized && !configure.maximized; state.fullscreen = configure.fullscreen; state.maximized = configure.maximized; - + state.tiling = configure.tiling; if got_unmaximized { - configure.size = Some(state.windowed_bounds.size); - } else if !configure.fullscreen && !configure.maximized { + configure.size = Some(state.window_bounds.size); + } else if !configure.maximized { + configure.size = + compute_outer_size(state.inset, configure.size, state.tiling); + } + if !configure.fullscreen && !configure.maximized { if let Some(size) = configure.size { - state.windowed_bounds = Bounds { + state.window_bounds = Bounds { origin: Point::default(), size, }; } } - drop(state); if let Some(size) = configure.size { self.resize(size); @@ -340,8 +375,11 @@ impl WaylandWindowStatePtr { state.xdg_surface.ack_configure(serial); let request_frame_callback = !state.acknowledged_first_configure; state.acknowledged_first_configure = true; - drop(state); - self.frame(request_frame_callback); + + if request_frame_callback { + drop(state); + self.frame(true); + } } _ => {} } @@ -351,10 +389,21 @@ impl WaylandWindowStatePtr { match event { zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { - self.set_decoration_state(WaylandDecorationState::Server) + self.state.borrow_mut().decorations = WindowDecorations::Server; + if let Some(mut appearance_changed) = + self.callbacks.borrow_mut().appearance_changed.as_mut() + { + appearance_changed(); + } } WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => { - self.set_decoration_state(WaylandDecorationState::Client) + self.state.borrow_mut().decorations = WindowDecorations::Client; + // Update background to be transparent + if let Some(mut appearance_changed) = + self.callbacks.borrow_mut().appearance_changed.as_mut() + { + appearance_changed(); + } } WEnum::Value(_) => { log::warn!("Unknown decoration mode"); @@ -389,14 +438,44 @@ impl WaylandWindowStatePtr { Some(size(px(width as f32), px(height as f32))) }; - let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8)); - let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8)); + let states = extract_states::(&states); + + let mut tiling = Tiling::default(); + let mut fullscreen = false; + let mut maximized = false; + + for state in states { + match state { + xdg_toplevel::State::Maximized => { + maximized = true; + } + xdg_toplevel::State::Fullscreen => { + fullscreen = true; + } + xdg_toplevel::State::TiledTop => { + tiling.top = true; + } + xdg_toplevel::State::TiledLeft => { + tiling.left = true; + } + xdg_toplevel::State::TiledRight => { + tiling.right = true; + } + xdg_toplevel::State::TiledBottom => { + tiling.bottom = true; + } + _ => { + // noop + } + } + } let mut state = self.state.borrow_mut(); state.in_progress_configure = Some(InProgressConfigure { size, fullscreen, maximized, + tiling, }); false @@ -415,6 +494,33 @@ impl WaylandWindowStatePtr { true } } + xdg_toplevel::Event::WmCapabilities { capabilities } => { + let mut window_controls = WindowControls::default(); + + let states = extract_states::(&capabilities); + + for state in states { + match state { + xdg_toplevel::WmCapabilities::Maximize => { + window_controls.maximize = true; + } + xdg_toplevel::WmCapabilities::Minimize => { + window_controls.minimize = true; + } + xdg_toplevel::WmCapabilities::Fullscreen => { + window_controls.fullscreen = true; + } + xdg_toplevel::WmCapabilities::WindowMenu => { + window_controls.window_menu = true; + } + _ => {} + } + } + + let mut state = self.state.borrow_mut(); + state.in_progress_window_controls = Some(window_controls); + false + } _ => false, } } @@ -545,18 +651,6 @@ impl WaylandWindowStatePtr { self.set_size_and_scale(None, Some(scale)); } - /// Notifies the window of the state of the decorations. - /// - /// # Note - /// - /// This API is indirectly called by the wayland compositor and - /// not meant to be called by a user who wishes to change the state - /// of the decorations. This is because the state of the decorations - /// is managed by the compositor and not the client. - pub fn set_decoration_state(&self, state: WaylandDecorationState) { - self.state.borrow_mut().decoration_state = state; - } - pub fn close(&self) { let mut callbacks = self.callbacks.borrow_mut(); if let Some(fun) = callbacks.close.take() { @@ -599,6 +693,17 @@ impl WaylandWindowStatePtr { } } +fn extract_states<'a, S: TryFrom + 'a>(states: &'a [u8]) -> impl Iterator + 'a +where + >::Error: 'a, +{ + states + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .flat_map(S::try_from) +} + fn primary_output_scale(state: &mut RefMut) -> i32 { let mut scale = 1; let mut current_output = state.display.take(); @@ -639,9 +744,9 @@ impl PlatformWindow for WaylandWindow { fn window_bounds(&self) -> WindowBounds { let state = self.borrow(); if state.fullscreen { - WindowBounds::Fullscreen(state.windowed_bounds) + WindowBounds::Fullscreen(state.window_bounds) } else if state.maximized { - WindowBounds::Maximized(state.windowed_bounds) + WindowBounds::Maximized(state.window_bounds) } else { drop(state); WindowBounds::Windowed(self.bounds()) @@ -718,52 +823,10 @@ impl PlatformWindow for WaylandWindow { self.borrow().toplevel.set_app_id(app_id.to_owned()); } - fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { - let opaque = background_appearance == WindowBackgroundAppearance::Opaque; + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { let mut state = self.borrow_mut(); - state.renderer.update_transparency(!opaque); - - let region = state - .globals - .compositor - .create_region(&state.globals.qh, ()); - region.add(0, 0, i32::MAX, i32::MAX); - - if opaque { - // Promise the compositor that this region of the window surface - // contains no transparent pixels. This allows the compositor to - // do skip whatever is behind the surface for better performance. - state.surface.set_opaque_region(Some(®ion)); - } else { - state.surface.set_opaque_region(None); - } - - if let Some(ref blur_manager) = state.globals.blur_manager { - if background_appearance == WindowBackgroundAppearance::Blurred { - if state.blur.is_none() { - let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); - blur.set_region(Some(®ion)); - state.blur = Some(blur); - } - state.blur.as_ref().unwrap().commit(); - } else { - // It probably doesn't hurt to clear the blur for opaque windows - blur_manager.unset(&state.surface); - if let Some(b) = state.blur.take() { - b.release() - } - } - } - - region.destroy(); - } - - fn set_edited(&mut self, _edited: bool) { - log::info!("ignoring macOS specific set_edited"); - } - - fn show_character_palette(&self) { - log::info!("ignoring macOS specific show_character_palette"); + state.background_appearance = background_appearance; + update_window(state); } fn minimize(&self) { @@ -831,6 +894,25 @@ impl PlatformWindow for WaylandWindow { fn completed_frame(&self) { let mut state = self.borrow_mut(); + if let Some(area) = state.requested_inset { + state.inset = Some(area); + } + + let window_geometry = inset_by_tiling( + state.bounds.map_origin(|_| px(0.0)), + state.inset.unwrap_or(px(0.0)), + state.tiling, + ) + .map(|v| v.0 as i32) + .map_size(|v| if v <= 0 { 1 } else { v }); + + state.xdg_surface.set_window_geometry( + window_geometry.origin.x, + window_geometry.origin.y, + window_geometry.size.width, + window_geometry.size.height, + ); + state.surface.commit(); } @@ -850,22 +932,173 @@ impl PlatformWindow for WaylandWindow { ); } - fn start_system_move(&self) { + fn start_window_move(&self) { let state = self.borrow(); let serial = state.client.get_serial(SerialKind::MousePress); state.toplevel._move(&state.globals.seat, serial); } - fn should_render_window_controls(&self) -> bool { - self.borrow().decoration_state == WaylandDecorationState::Client + fn start_window_resize(&self, edge: crate::ResizeEdge) { + let state = self.borrow(); + state.toplevel.resize( + &state.globals.seat, + state.client.get_serial(SerialKind::MousePress), + edge.to_xdg(), + ) + } + + fn window_decorations(&self) -> Decorations { + let state = self.borrow(); + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => Decorations::Client { + tiling: state.tiling, + }, + } + } + + fn request_decorations(&self, decorations: WindowDecorations) { + let mut state = self.borrow_mut(); + state.decorations = decorations; + if let Some(decoration) = state.decoration.as_ref() { + decoration.set_mode(decorations.to_xdg()); + update_window(state); + } + } + + fn window_controls(&self) -> WindowControls { + self.borrow().window_controls + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.borrow_mut(); + if Some(inset) != state.inset { + state.requested_inset = Some(inset); + update_window(state); + } + } +} + +fn update_window(mut state: RefMut) { + let opaque = !state.is_transparent(); + + state.renderer.update_transparency(!opaque); + let mut opaque_area = state.window_bounds.map(|v| v.0 as i32); + if let Some(inset) = state.inset { + opaque_area.inset(inset.0 as i32); + } + + let region = state + .globals + .compositor + .create_region(&state.globals.qh, ()); + region.add( + opaque_area.origin.x, + opaque_area.origin.y, + opaque_area.size.width, + opaque_area.size.height, + ); + + // Note that rounded corners make this rectangle API hard to work with. + // As this is common when using CSD, let's just disable this API. + if state.background_appearance == WindowBackgroundAppearance::Opaque + && state.decorations == WindowDecorations::Server + { + // Promise the compositor that this region of the window surface + // contains no transparent pixels. This allows the compositor to + // do skip whatever is behind the surface for better performance. + state.surface.set_opaque_region(Some(®ion)); + } else { + state.surface.set_opaque_region(None); + } + + if let Some(ref blur_manager) = state.globals.blur_manager { + if state.background_appearance == WindowBackgroundAppearance::Blurred { + if state.blur.is_none() { + let blur = blur_manager.create(&state.surface, &state.globals.qh, ()); + blur.set_region(Some(®ion)); + state.blur = Some(blur); + } + state.blur.as_ref().unwrap().commit(); + } else { + // It probably doesn't hurt to clear the blur for opaque windows + blur_manager.unset(&state.surface); + if let Some(b) = state.blur.take() { + b.release() + } + } + } + + region.destroy(); +} + +impl WindowDecorations { + fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode { + match self { + WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, + WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + } } } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum WaylandDecorationState { - /// Decorations are to be provided by the client - Client, +impl ResizeEdge { + fn to_xdg(&self) -> xdg_toplevel::ResizeEdge { + match self { + ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top, + ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight, + ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right, + ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight, + ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom, + ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft, + ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left, + ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft, + } + } +} + +/// The configuration event is in terms of the window geometry, which we are constantly +/// updating to account for the client decorations. But that's not the area we want to render +/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets +fn compute_outer_size( + inset: Option, + new_size: Option>, + tiling: Tiling, +) -> Option> { + let Some(inset) = inset else { return new_size }; + + new_size.map(|mut new_size| { + if !tiling.top { + new_size.height += inset; + } + if !tiling.bottom { + new_size.height += inset; + } + if !tiling.left { + new_size.width += inset; + } + if !tiling.right { + new_size.width += inset; + } + + new_size + }) +} + +fn inset_by_tiling(mut bounds: Bounds, inset: Pixels, tiling: Tiling) -> Bounds { + if !tiling.top { + bounds.origin.y += inset; + bounds.size.height -= inset; + } + if !tiling.bottom { + bounds.size.height -= inset; + } + if !tiling.left { + bounds.origin.x += inset; + bounds.size.width -= inset; + } + if !tiling.right { + bounds.size.width -= inset; + } - /// Decorations are provided by the server - Server, + bounds } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index ba42bc3fa4f49f..39c0b0fd6dcd14 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -512,7 +512,7 @@ impl X11Client { match event { Event::ClientMessage(event) => { let window = self.get_window(event.window)?; - let [atom, ..] = event.data.as_data32(); + let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32(); let mut state = self.0.borrow_mut(); if atom == state.atoms.WM_DELETE_WINDOW { @@ -521,6 +521,12 @@ impl X11Client { // Rest of the close logic is handled in drop_window() window.close(); } + } else if atom == state.atoms._NET_WM_SYNC_REQUEST { + window.state.borrow_mut().last_sync_counter = + Some(x11rb::protocol::sync::Int64 { + lo: arg2, + hi: arg3 as i32, + }) } } Event::ConfigureNotify(event) => { @@ -537,6 +543,10 @@ impl X11Client { let window = self.get_window(event.window)?; window.configure(bounds); } + Event::PropertyNotify(event) => { + let window = self.get_window(event.window)?; + window.property_notify(event); + } Event::Expose(event) => { let window = self.get_window(event.window)?; window.refresh(); diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index b15de3df7327e5..52fccaf272c087 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -2,10 +2,11 @@ use anyhow::Context; use crate::{ platform::blade::{BladeRenderer, BladeSurfaceConfig}, - px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, - PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, - WindowKind, WindowParams, X11ClientStatePtr, + px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance, + WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams, + X11ClientStatePtr, }; use blade_graphics as gpu; @@ -15,24 +16,17 @@ use x11rb::{ connection::Connection, protocol::{ randr::{self, ConnectionExt as _}, + sync, xinput::{self, ConnectionExt as _}, - xproto::{ - self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply, - }, + xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply}, }, wrapper::ConnectionExt as _, xcb_ffi::XCBConnection, }; use std::{ - cell::RefCell, - ffi::c_void, - num::NonZeroU32, - ops::Div, - ptr::NonNull, - rc::Rc, - sync::{self, Arc}, - time::Duration, + cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, + sync::Arc, time::Duration, }; use super::{X11Display, XINPUT_MASTER_DEVICE}; @@ -50,10 +44,16 @@ x11rb::atom_manager! { _NET_WM_STATE_HIDDEN, _NET_WM_STATE_FOCUSED, _NET_ACTIVE_WINDOW, + _NET_WM_SYNC_REQUEST, + _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_BYPASS_COMPOSITOR, _NET_WM_MOVERESIZE, _NET_WM_WINDOW_TYPE, _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_SYNC, + _MOTIF_WM_HINTS, _GTK_SHOW_WINDOW_MENU, + _GTK_FRAME_EXTENTS, } } @@ -70,6 +70,21 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) } } +impl ResizeEdge { + fn to_moveresize(&self) -> u32 { + match self { + ResizeEdge::TopLeft => 0, + ResizeEdge::Top => 1, + ResizeEdge::TopRight => 2, + ResizeEdge::Right => 3, + ResizeEdge::BottomRight => 4, + ResizeEdge::Bottom => 5, + ResizeEdge::BottomLeft => 6, + ResizeEdge::Left => 7, + } + } +} + #[derive(Debug)] struct Visual { id: xproto::Visualid, @@ -166,6 +181,8 @@ pub struct X11WindowState { executor: ForegroundExecutor, atoms: XcbAtoms, x_root_window: xproto::Window, + pub(crate) counter_id: sync::Counter, + pub(crate) last_sync_counter: Option, _raw: RawWindow, bounds: Bounds, scale_factor: f32, @@ -173,7 +190,22 @@ pub struct X11WindowState { display: Rc, input_handler: Option, appearance: WindowAppearance, + background_appearance: WindowBackgroundAppearance, + maximized_vertical: bool, + maximized_horizontal: bool, + hidden: bool, + active: bool, + fullscreen: bool, + decorations: WindowDecorations, pub handle: AnyWindowHandle, + last_insets: [u32; 4], +} + +impl X11WindowState { + fn is_transparent(&self) -> bool { + self.decorations == WindowDecorations::Client + || self.background_appearance != WindowBackgroundAppearance::Opaque + } } #[derive(Clone)] @@ -230,19 +262,11 @@ impl X11WindowState { .map_or(x_main_screen_index, |did| did.0 as usize); let visual_set = find_visuals(&xcb_connection, x_screen_index); - let visual_maybe = match params.window_background { - WindowBackgroundAppearance::Opaque => visual_set.opaque, - WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => { - visual_set.transparent - } - }; - let visual = match visual_maybe { + + let visual = match visual_set.transparent { Some(visual) => visual, None => { - log::warn!( - "Unable to find a matching visual for {:?}", - params.window_background - ); + log::warn!("Unable to find a transparent visual",); visual_set.inherit } }; @@ -269,7 +293,8 @@ impl X11WindowState { | xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::FOCUS_CHANGE | xproto::EventMask::KEY_PRESS - | xproto::EventMask::KEY_RELEASE, + | xproto::EventMask::KEY_RELEASE + | EventMask::PROPERTY_CHANGE, ); let mut bounds = params.bounds.to_device_pixels(scale_factor); @@ -349,7 +374,26 @@ impl X11WindowState { x_window, atoms.WM_PROTOCOLS, xproto::AtomEnum::ATOM, - &[atoms.WM_DELETE_WINDOW], + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ) + .unwrap(); + + sync::initialize(xcb_connection, 3, 1).unwrap(); + let sync_request_counter = xcb_connection.generate_id().unwrap(); + sync::create_counter( + xcb_connection, + sync_request_counter, + sync::Int64 { lo: 0, hi: 0 }, + ) + .unwrap(); + + xcb_connection + .change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], ) .unwrap(); @@ -396,7 +440,8 @@ impl X11WindowState { // Note: this has to be done after the GPU init, or otherwise // the sizes are immediately invalidated. size: query_render_extent(xcb_connection, x_window), - transparent: params.window_background != WindowBackgroundAppearance::Opaque, + // In case we have window decorations to render + transparent: true, }; xcb_connection.map_window(x_window).unwrap(); @@ -438,9 +483,19 @@ impl X11WindowState { renderer: BladeRenderer::new(gpu, config), atoms: *atoms, input_handler: None, + active: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, appearance, handle, + background_appearance: WindowBackgroundAppearance::Opaque, destroyed: false, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + counter_id: sync_request_counter, + last_sync_counter: None, refresh_rate, }) } @@ -511,7 +566,7 @@ impl X11Window { scale_factor: f32, appearance: WindowAppearance, ) -> anyhow::Result { - Ok(Self(X11WindowStatePtr { + let ptr = X11WindowStatePtr { state: Rc::new(RefCell::new(X11WindowState::new( handle, client, @@ -527,7 +582,12 @@ impl X11Window { callbacks: Rc::new(RefCell::new(Callbacks::default())), xcb_connection: xcb_connection.clone(), x_window, - })) + }; + + let state = ptr.state.borrow_mut(); + ptr.set_wm_properties(state); + + Ok(Self(ptr)) } fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) { @@ -549,29 +609,6 @@ impl X11Window { .unwrap(); } - fn get_wm_hints(&self) -> Vec { - let reply = self - .0 - .xcb_connection - .get_property( - false, - self.0.x_window, - self.0.state.borrow().atoms._NET_WM_STATE, - xproto::AtomEnum::ATOM, - 0, - u32::MAX, - ) - .unwrap() - .reply() - .unwrap(); - // Reply is in u8 but atoms are represented as u32 - reply - .value - .chunks_exact(4) - .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect() - } - fn get_root_position(&self, position: Point) -> TranslateCoordinatesReply { let state = self.0.state.borrow(); self.0 @@ -586,6 +623,48 @@ impl X11Window { .reply() .unwrap() } + + fn send_moveresize(&self, flag: u32) { + let state = self.0.state.borrow(); + + self.0 + .xcb_connection + .ungrab_pointer(x11rb::CURRENT_TIME) + .unwrap() + .check() + .unwrap(); + + let pointer = self + .0 + .xcb_connection + .query_pointer(self.0.x_window) + .unwrap() + .reply() + .unwrap(); + let message = ClientMessageEvent::new( + 32, + self.0.x_window, + state.atoms._NET_WM_MOVERESIZE, + [ + pointer.root_x as u32, + pointer.root_y as u32, + flag, + 0, // Left mouse button + 0, + ], + ); + self.0 + .xcb_connection + .send_event( + false, + state.x_root_window, + EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, + message, + ) + .unwrap(); + + self.0.xcb_connection.flush().unwrap(); + } } impl X11WindowStatePtr { @@ -600,6 +679,54 @@ impl X11WindowStatePtr { } } + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) { + let mut state = self.state.borrow_mut(); + if event.atom == state.atoms._NET_WM_STATE { + self.set_wm_properties(state); + } + } + + fn set_wm_properties(&self, mut state: std::cell::RefMut) { + let reply = self + .xcb_connection + .get_property( + false, + self.x_window, + state.atoms._NET_WM_STATE, + xproto::AtomEnum::ATOM, + 0, + u32::MAX, + ) + .unwrap() + .reply() + .unwrap(); + + let atoms = reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])); + + state.active = false; + state.fullscreen = false; + state.maximized_vertical = false; + state.maximized_horizontal = false; + state.hidden = true; + + for atom in atoms { + if atom == state.atoms._NET_WM_STATE_FOCUSED { + state.active = true; + } else if atom == state.atoms._NET_WM_STATE_FULLSCREEN { + state.fullscreen = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT { + state.maximized_vertical = true; + } else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ { + state.maximized_horizontal = true; + } else if atom == state.atoms._NET_WM_STATE_HIDDEN { + state.hidden = true; + } + } + } + pub fn close(&self) { let mut callbacks = self.callbacks.borrow_mut(); if let Some(fun) = callbacks.close.take() { @@ -715,6 +842,9 @@ impl X11WindowStatePtr { )); resize_args = Some((state.content_size(), state.scale_factor)); } + if let Some(value) = state.last_sync_counter.take() { + sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap(); + } } let mut callbacks = self.callbacks.borrow_mut(); @@ -737,8 +867,12 @@ impl X11WindowStatePtr { } pub fn set_appearance(&mut self, appearance: WindowAppearance) { - self.state.borrow_mut().appearance = appearance; - + let mut state = self.state.borrow_mut(); + state.appearance = appearance; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + state.appearance = appearance; + drop(state); let mut callbacks = self.callbacks.borrow_mut(); if let Some(ref mut fun) = callbacks.appearance_changed { (fun)() @@ -757,11 +891,9 @@ impl PlatformWindow for X11Window { fn is_maximized(&self) -> bool { let state = self.0.state.borrow(); - let wm_hints = self.get_wm_hints(); + // A maximized window that gets minimized will still retain its maximized state. - !wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN) - && wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT) - && wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ) + !state.hidden && state.maximized_vertical && state.maximized_horizontal } fn window_bounds(&self) -> WindowBounds { @@ -862,9 +994,7 @@ impl PlatformWindow for X11Window { } fn is_active(&self) -> bool { - let state = self.0.state.borrow(); - self.get_wm_hints() - .contains(&state.atoms._NET_WM_STATE_FOCUSED) + self.0.state.borrow().active } fn set_title(&mut self, title: &str) { @@ -913,10 +1043,11 @@ impl PlatformWindow for X11Window { log::info!("ignoring macOS specific set_edited"); } - fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { - let mut inner = self.0.state.borrow_mut(); - let transparent = background_appearance != WindowBackgroundAppearance::Opaque; - inner.renderer.update_transparency(transparent); + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { + let mut state = self.0.state.borrow_mut(); + state.background_appearance = background_appearance; + let transparent = state.is_transparent(); + state.renderer.update_transparency(transparent); } fn show_character_palette(&self) { @@ -962,9 +1093,7 @@ impl PlatformWindow for X11Window { } fn is_fullscreen(&self) -> bool { - let state = self.0.state.borrow(); - self.get_wm_hints() - .contains(&state.atoms._NET_WM_STATE_FULLSCREEN) + self.0.state.borrow().fullscreen } fn on_request_frame(&self, callback: Box) { @@ -1004,7 +1133,7 @@ impl PlatformWindow for X11Window { inner.renderer.draw(scene); } - fn sprite_atlas(&self) -> sync::Arc { + fn sprite_atlas(&self) -> Arc { let inner = self.0.state.borrow(); inner.renderer.sprite_atlas().clone() } @@ -1035,41 +1164,109 @@ impl PlatformWindow for X11Window { .unwrap(); } - fn start_system_move(&self) { - let state = self.0.state.borrow(); - let pointer = self - .0 - .xcb_connection - .query_pointer(self.0.x_window) - .unwrap() - .reply() - .unwrap(); + fn start_window_move(&self) { const MOVERESIZE_MOVE: u32 = 8; - let message = ClientMessageEvent::new( - 32, - self.0.x_window, - state.atoms._NET_WM_MOVERESIZE, - [ - pointer.root_x as u32, - pointer.root_y as u32, - MOVERESIZE_MOVE, - 1, // Left mouse button - 1, - ], - ); + self.send_moveresize(MOVERESIZE_MOVE); + } + + fn start_window_resize(&self, edge: ResizeEdge) { + self.send_moveresize(edge.to_moveresize()); + } + + fn window_decorations(&self) -> crate::Decorations { + let state = self.0.state.borrow(); + + match state.decorations { + WindowDecorations::Server => Decorations::Server, + WindowDecorations::Client => { + // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d + Decorations::Client { + tiling: Tiling { + top: state.maximized_vertical, + bottom: state.maximized_vertical, + left: state.maximized_horizontal, + right: state.maximized_horizontal, + }, + } + } + } + } + + fn set_client_inset(&self, inset: Pixels) { + let mut state = self.0.state.borrow_mut(); + + let dp = (inset.0 * state.scale_factor) as u32; + + let (left, right) = if state.maximized_horizontal { + (0, 0) + } else { + (dp, dp) + }; + let (top, bottom) = if state.maximized_vertical { + (0, 0) + } else { + (dp, dp) + }; + let insets = [left, right, top, bottom]; + + if state.last_insets != insets { + state.last_insets = insets; + + self.0 + .xcb_connection + .change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._GTK_FRAME_EXTENTS, + xproto::AtomEnum::CARDINAL, + size_of::() as u8 * 8, + 4, + bytemuck::cast_slice::(&insets), + ) + .unwrap(); + } + } + + fn request_decorations(&self, decorations: crate::WindowDecorations) { + // https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87 + let hints_data: [u32; 5] = match decorations { + WindowDecorations::Server => [1 << 1, 0, 1, 0, 0], + WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], + }; + + let mut state = self.0.state.borrow_mut(); + self.0 .xcb_connection - .send_event( - false, - state.x_root_window, - EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, - message, + .change_property( + xproto::PropMode::REPLACE, + self.0.x_window, + state.atoms._MOTIF_WM_HINTS, + state.atoms._MOTIF_WM_HINTS, + std::mem::size_of::() as u8 * 8, + 5, + bytemuck::cast_slice::(&hints_data), ) .unwrap(); - } - fn should_render_window_controls(&self) -> bool { - false + match decorations { + WindowDecorations::Server => { + state.decorations = WindowDecorations::Server; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + WindowDecorations::Client => { + state.decorations = WindowDecorations::Client; + let is_transparent = state.is_transparent(); + state.renderer.update_transparency(is_transparent); + } + } + + drop(state); + let mut callbacks = self.0.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); + } } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d6fc06b43c80c9..332cf74bd5e787 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -796,14 +796,24 @@ impl Platform for MacPlatform { CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor], CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor], CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], + CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor], CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor], CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor], - CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor], CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor], + CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor], CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor], - CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor], - CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor], + + // Undocumented, private class methods: + // https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor + CursorStyle::ResizeUpLeftDownRight => { + msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor] + } + CursorStyle::ResizeUpRightDownLeft => { + msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor] + } + CursorStyle::IBeamCursorForVerticalLayout => { msg_send![class!(NSCursor), IBeamCursorForVerticalLayout] } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 5eb9caba2e2f46..e98e8f0f7479e9 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -497,7 +497,6 @@ impl MacWindow { pub fn open( handle: AnyWindowHandle, WindowParams { - window_background, bounds, titlebar, kind, @@ -603,7 +602,7 @@ impl MacWindow { native_window as *mut _, native_view as *mut _, bounds.size.map(|pixels| pixels.0), - window_background != WindowBackgroundAppearance::Opaque, + false, ), request_frame_callback: None, event_callback: None, @@ -676,8 +675,6 @@ impl MacWindow { native_window.setContentView_(native_view.autorelease()); native_window.makeFirstResponder_(native_view); - window.set_background_appearance(window_background); - match kind { WindowKind::Normal => { native_window.setLevel_(NSNormalWindowLevel); @@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow { fn set_app_id(&mut self, _app_id: &str) {} - fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { let mut this = self.0.as_ref().lock(); this.renderer .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); @@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow { fn sprite_atlas(&self) -> Arc { self.0.lock().renderer.sprite_atlas().clone() } - - fn show_window_menu(&self, _position: Point) {} - - fn start_system_move(&self) {} - - fn should_render_window_controls(&self) -> bool { - false - } } impl rwh::HasWindowHandle for MacWindow { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 5946256a8d9b31..7ffaf77250b309 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -188,9 +188,7 @@ impl PlatformWindow for TestWindow { fn set_app_id(&mut self, _app_id: &str) {} - fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) { - unimplemented!() - } + fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {} fn set_edited(&mut self, edited: bool) { self.0.lock().edited = edited; @@ -262,13 +260,9 @@ impl PlatformWindow for TestWindow { unimplemented!() } - fn start_system_move(&self) { + fn start_window_move(&self) { unimplemented!() } - - fn should_render_window_controls(&self) -> bool { - false - } } pub(crate) struct TestAtlasState { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index eedb32cc2e638f..8c648661cafea8 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -274,7 +274,7 @@ impl WindowsWindow { handle, hide_title_bar, display, - transparent: params.window_background != WindowBackgroundAppearance::Opaque, + transparent: true, executor, current_cursor, }; @@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow { .ok(); } - fn set_app_id(&mut self, _app_id: &str) {} - - fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { self.0 .state .borrow_mut() @@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow { .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque); } - // todo(windows) - fn set_edited(&mut self, _edited: bool) {} - - // todo(windows) - fn show_character_palette(&self) {} - fn minimize(&self) { unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() }; } @@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow { fn get_raw_handle(&self) -> HWND { self.0.hwnd } - - fn show_window_menu(&self, _position: Point) {} - - fn start_system_move(&self) {} - - fn should_render_window_controls(&self) -> bool { - false - } } #[implement(IDropTarget)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 08c4a8753b616a..a5f2af6035da2d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,19 +1,20 @@ use crate::{ 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, WindowBounds, WindowOptions, WindowParams, - WindowTextSystem, SUBPIXEL_VARIANTS, + Context, Corners, CursorStyle, Decorations, 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, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, + Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, + TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View, + VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, + WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, + SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::{FxHashMap, FxHashSet}; @@ -610,7 +611,10 @@ fn default_bounds(display_id: Option, cx: &mut AppContext) -> Bounds< cx.active_window() .and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok()) - .map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET)) + .map(|mut bounds| { + bounds.origin += DEFAULT_WINDOW_OFFSET; + bounds + }) .unwrap_or_else(|| { let display = display_id .map(|id| cx.find_display(id)) @@ -639,6 +643,7 @@ impl Window { window_background, app_id, window_min_size, + window_decorations, } = options; let bounds = window_bounds @@ -654,7 +659,6 @@ impl Window { focus, show, display_id, - window_background, window_min_size, }, )?; @@ -672,6 +676,10 @@ impl Window { let next_frame_callbacks: Rc>> = Default::default(); let last_input_timestamp = Rc::new(Cell::new(Instant::now())); + platform_window + .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); + platform_window.set_background_appearance(window_background); + if let Some(ref window_open_state) = window_bounds { match window_open_state { WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(), @@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> { self.window.platform_window.is_maximized() } + /// request a certain window decoration (Wayland) + pub fn request_decorations(&self, decorations: WindowDecorations) { + self.window.platform_window.request_decorations(decorations); + } + + /// Start a window resize operation (Wayland) + pub fn start_window_resize(&self, edge: ResizeEdge) { + self.window.platform_window.start_window_resize(edge); + } + /// Return the `WindowBounds` to indicate that how a window should be opened /// after it has been closed pub fn window_bounds(&self) -> WindowBounds { @@ -1217,13 +1235,23 @@ impl<'a> WindowContext<'a> { /// Tells the compositor to take control of window movement (Wayland and X11) /// /// Events may not be received during a move operation. - pub fn start_system_move(&self) { - self.window.platform_window.start_system_move() + pub fn start_window_move(&self) { + self.window.platform_window.start_window_move() + } + + /// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11) + pub fn set_client_inset(&self, inset: Pixels) { + self.window.platform_window.set_client_inset(inset); } /// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11) - pub fn should_render_window_controls(&self) -> bool { - self.window.platform_window.should_render_window_controls() + pub fn window_decorations(&self) -> Decorations { + self.window.platform_window.window_decorations() + } + + /// Returns which window controls are currently visible (Wayland) + pub fn window_controls(&self) -> WindowControls { + self.window.platform_window.window_controls() } /// Updates the window's title at the platform level. @@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> { } /// Sets the window background appearance. - pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) { + pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { self.window .platform_window .set_background_appearance(background_appearance); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index fa54159f610318..7bbe7c9885c517 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -28,7 +28,8 @@ pub use settings::*; pub use styles::*; use gpui::{ - AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance, + px, AppContext, AssetSource, Hsla, Pixels, SharedString, WindowAppearance, + WindowBackgroundAppearance, }; use serde::Deserialize; @@ -38,6 +39,9 @@ pub enum Appearance { Dark, } +pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); +pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); + impl Appearance { pub fn is_light(&self) -> bool { match self { diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs index 2f0f9a53929757..67e87d45ea5d29 100644 --- a/crates/title_bar/src/platforms.rs +++ b/crates/title_bar/src/platforms.rs @@ -1,4 +1,3 @@ -pub mod platform_generic; pub mod platform_linux; pub mod platform_mac; pub mod platform_windows; diff --git a/crates/title_bar/src/platforms/platform_generic.rs b/crates/title_bar/src/platforms/platform_generic.rs deleted file mode 100644 index 42e32de4e9e316..00000000000000 --- a/crates/title_bar/src/platforms/platform_generic.rs +++ /dev/null @@ -1,47 +0,0 @@ -use gpui::{prelude::*, Action}; - -use ui::prelude::*; - -use crate::window_controls::{WindowControl, WindowControlType}; - -#[derive(IntoElement)] -pub struct GenericWindowControls { - close_window_action: Box, -} - -impl GenericWindowControls { - pub fn new(close_action: Box) -> Self { - Self { - close_window_action: close_action, - } - } -} - -impl RenderOnce for GenericWindowControls { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - h_flex() - .id("generic-window-controls") - .px_3() - .gap_1p5() - .child(WindowControl::new( - "minimize", - WindowControlType::Minimize, - cx, - )) - .child(WindowControl::new( - "maximize-or-restore", - if cx.is_maximized() { - WindowControlType::Restore - } else { - WindowControlType::Maximize - }, - cx, - )) - .child(WindowControl::new_close( - "close", - WindowControlType::Close, - self.close_window_action, - cx, - )) - } -} diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs index c2142fc8d54dab..dd71e596256655 100644 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ b/crates/title_bar/src/platforms/platform_linux.rs @@ -2,7 +2,7 @@ use gpui::{prelude::*, Action}; use ui::prelude::*; -use super::platform_generic::GenericWindowControls; +use crate::window_controls::{WindowControl, WindowControlType}; #[derive(IntoElement)] pub struct LinuxWindowControls { @@ -18,7 +18,30 @@ impl LinuxWindowControls { } impl RenderOnce for LinuxWindowControls { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element() + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + h_flex() + .id("generic-window-controls") + .px_3() + .gap_3() + .child(WindowControl::new( + "minimize", + WindowControlType::Minimize, + cx, + )) + .child(WindowControl::new( + "maximize-or-restore", + if cx.is_maximized() { + WindowControlType::Restore + } else { + WindowControlType::Maximize + }, + cx, + )) + .child(WindowControl::new_close( + "close", + WindowControlType::Close, + self.close_window_action, + cx, + )) } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index e218305905e566..59854ab57228f6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -9,9 +9,9 @@ use call::{ActiveCall, ParticipantLocation}; use client::{Client, UserStore}; use collab::render_color_ribbon; use gpui::{ - actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity, - IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, - Subscription, ViewContext, VisualContext, WeakView, + actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement, + Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful, + StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, }; use project::{Project, RepositoryEntry}; use recent_projects::RecentProjects; @@ -58,6 +58,7 @@ pub struct TitleBar { user_store: Model, client: Arc, workspace: WeakView, + should_move: bool, _subscriptions: Vec, } @@ -73,8 +74,10 @@ impl Render for TitleBar { let platform_supported = cfg!(target_os = "macos"); let height = Self::height(cx); + let supported_controls = cx.window_controls(); + let decorations = cx.window_decorations(); - let mut title_bar = h_flex() + h_flex() .id("titlebar") .w_full() .pt(Self::top_padding(cx)) @@ -88,6 +91,16 @@ impl Render for TitleBar { this.pl_2() } }) + .map(|el| { + match decorations { + Decorations::Server => el, + Decorations::Client { tiling, .. } => el + .when(!(tiling.top || tiling.right), |el| { + el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)) + } + }) .bg(cx.theme().colors().title_bar_background) .content_stretch() .child( @@ -113,7 +126,7 @@ impl Render for TitleBar { .children(self.render_project_host(cx)) .child(self.render_project_name(cx)) .children(self.render_project_branch(cx)) - .on_mouse_move(|_, cx| cx.stop_propagation()), + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) ) .child( h_flex() @@ -145,7 +158,7 @@ impl Render for TitleBar { this.children(current_user_face_pile.map(|face_pile| { v_flex() - .on_mouse_move(|_, cx| cx.stop_propagation()) + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) .child(face_pile) .child(render_color_ribbon(player_colors.local().cursor)) })) @@ -208,7 +221,7 @@ impl Render for TitleBar { h_flex() .gap_1() .pr_1() - .on_mouse_move(|_, cx| cx.stop_propagation()) + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) .when_some(room, |this, room| { let room = room.read(cx); let project = self.project.read(cx); @@ -373,34 +386,38 @@ impl Render for TitleBar { } }), ) - ); - // Windows Window Controls - title_bar = title_bar.when( + ).when( self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(), |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)), - ); - - // Linux Window Controls - title_bar = title_bar.when( + ).when( self.platform_style == PlatformStyle::Linux && !cx.is_fullscreen() - && cx.should_render_window_controls(), + && matches!(decorations, Decorations::Client { .. }), |title_bar| { title_bar .child(platform_linux::LinuxWindowControls::new(close_action)) - .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| { - cx.show_window_menu(ev.position) - }) - .on_mouse_move(move |ev, cx| { - if ev.dragging() { - cx.start_system_move(); - } + .when(supported_controls.window_menu, |titlebar| { + titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| { + cx.show_window_menu(ev.position) + }) }) - }, - ); - title_bar + .on_mouse_move(cx.listener(move |this, _ev, cx| { + if this.should_move { + this.should_move = false; + cx.start_window_move(); + } + })) + .on_mouse_down_out(cx.listener(move |this, _ev, _cx| { + this.should_move = false; + })) + .on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| { + this.should_move = true; + })) + + }, + ) } } @@ -430,6 +447,7 @@ impl TitleBar { content: div().id(id.into()), children: SmallVec::new(), workspace: workspace.weak_handle(), + should_move: false, project, user_store, client, diff --git a/crates/title_bar/src/window_controls.rs b/crates/title_bar/src/window_controls.rs index 5b44f0c4461d55..21b3811668553a 100644 --- a/crates/title_bar/src/window_controls.rs +++ b/crates/title_bar/src/window_controls.rs @@ -38,7 +38,7 @@ impl WindowControlStyle { Self { background: colors.ghost_element_background, - background_hover: colors.ghost_element_background, + background_hover: colors.ghost_element_hover, icon: colors.icon, icon_hover: colors.icon_muted, } @@ -127,7 +127,7 @@ impl WindowControl { impl RenderOnce for WindowControl { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { let icon = svg() - .size_5() + .size_4() .flex_none() .path(self.icon.icon().path()) .text_color(self.style.icon) @@ -139,7 +139,7 @@ impl RenderOnce for WindowControl { .cursor_pointer() .justify_center() .content_center() - .rounded_md() + .rounded_2xl() .w_5() .h_5() .hover(|this| this.bg(self.style.background_hover)) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 0b80126163ee2e..f2d2b73854e067 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,9 +1,10 @@ use crate::{ItemHandle, Pane}; use gpui::{ - AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, - WindowContext, + AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View, + ViewContext, WindowContext, }; use std::any::TypeId; +use theme::CLIENT_SIDE_DECORATION_ROUNDING; use ui::{h_flex, prelude::*}; use util::ResultExt; @@ -40,8 +41,17 @@ impl Render for StatusBar { .gap(Spacing::Large.rems(cx)) .py(Spacing::Small.rems(cx)) .px(Spacing::Large.rems(cx)) - // .h_8() .bg(cx.theme().colors().status_bar_background) + .map(|el| match cx.window_decorations() { + Decorations::Server => el, + Decorations::Client { tiling, .. } => el + .when(!(tiling.bottom || tiling.right), |el| { + el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |el| { + el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }), + }) .child(self.render_left_tools(cx)) .child(self.render_right_tools(cx)) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f15666995662fb..afc494a6ea0ef1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -27,11 +27,13 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action, - AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, - DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, - KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, - Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions, + action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, + transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, + AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId, + EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView, + Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, + Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle, + WindowOptions, }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -4165,156 +4167,162 @@ impl Render for Workspace { let theme = cx.theme().clone(); let colors = theme.colors(); - self.actions(div(), cx) - .key_context(context) - .relative() - .size_full() - .flex() - .flex_col() - .font(ui_font) - .gap_0() - .justify_start() - .items_start() - .text_color(colors.text) - .bg(colors.background) - .children(self.titlebar_item.clone()) - .child( - div() - .id("workspace") - .relative() - .flex_1() - .w_full() - .flex() - .flex_col() - .overflow_hidden() - .border_t_1() - .border_b_1() - .border_color(colors.border) - .child({ - let this = cx.view().clone(); - canvas( - move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds), - |_, _, _| {}, - ) - .absolute() - .size_full() - }) - .when(self.zoomed.is_none(), |this| { - this.on_drag_move(cx.listener( - |workspace, e: &DragMoveEvent, cx| match e.drag(cx).0 { - DockPosition::Left => { - let size = workspace.bounds.left() + e.event.position.x; - workspace.left_dock.update(cx, |left_dock, cx| { - left_dock.resize_active_panel(Some(size), cx); - }); - } - DockPosition::Right => { - let size = workspace.bounds.right() - e.event.position.x; - workspace.right_dock.update(cx, |right_dock, cx| { - right_dock.resize_active_panel(Some(size), cx); - }); - } - DockPosition::Bottom => { - let size = workspace.bounds.bottom() - e.event.position.y; - workspace.bottom_dock.update(cx, |bottom_dock, cx| { - bottom_dock.resize_active_panel(Some(size), cx); - }); - } - }, - )) - }) - .child( - div() - .flex() - .flex_row() - .h_full() - // Left Dock - .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then( - || { - div() - .flex() - .flex_none() - .overflow_hidden() - .child(self.left_dock.clone()) + client_side_decorations( + self.actions(div(), cx) + .key_context(context) + .relative() + .size_full() + .flex() + .flex_col() + .font(ui_font) + .gap_0() + .justify_start() + .items_start() + .text_color(colors.text) + .overflow_hidden() + .children(self.titlebar_item.clone()) + .child( + div() + .id("workspace") + .bg(colors.background) + .relative() + .flex_1() + .w_full() + .flex() + .flex_col() + .overflow_hidden() + .border_t_1() + .border_b_1() + .border_color(colors.border) + .child({ + let this = cx.view().clone(); + canvas( + move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds), + |_, _, _| {}, + ) + .absolute() + .size_full() + }) + .when(self.zoomed.is_none(), |this| { + this.on_drag_move(cx.listener( + |workspace, e: &DragMoveEvent, cx| match e.drag(cx).0 { + DockPosition::Left => { + let size = e.event.position.x - workspace.bounds.left(); + workspace.left_dock.update(cx, |left_dock, cx| { + left_dock.resize_active_panel(Some(size), cx); + }); + } + DockPosition::Right => { + let size = workspace.bounds.right() - e.event.position.x; + workspace.right_dock.update(cx, |right_dock, cx| { + right_dock.resize_active_panel(Some(size), cx); + }); + } + DockPosition::Bottom => { + let size = workspace.bounds.bottom() - e.event.position.y; + workspace.bottom_dock.update(cx, |bottom_dock, cx| { + bottom_dock.resize_active_panel(Some(size), cx); + }); + } }, )) - // Panes - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| { - this.child(p.border_r_1()) - }) - .child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )) - .when_some(paddings.1, |this, p| { - this.child(p.border_l_1()) - }), - ) - .children( - self.zoomed_position - .ne(&Some(DockPosition::Bottom)) - .then(|| self.bottom_dock.clone()), - ), - ) - // Right Dock - .children(self.zoomed_position.ne(&Some(DockPosition::Right)).then( - || { + }) + .child( + div() + .flex() + .flex_row() + .h_full() + // Left Dock + .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then( + || { + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.left_dock.clone()) + }, + )) + // Panes + .child( div() .flex() - .flex_none() + .flex_col() + .flex_1() .overflow_hidden() - .child(self.right_dock.clone()) - }, - )), - ) - .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() - .occlude() - .absolute() - .overflow_hidden() - .border_color(colors.border) - .bg(colors.background) - .child(zoomed_view) - .inset_0() - .shadow_lg(); - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => div.top_2().bottom_2().left_2().right_2().border_1(), - }) - })) - .child(self.modal_layer.clone()) - .children(self.render_notifications(cx)), - ) - .child(self.status_bar.clone()) - .children(if self.project.read(cx).is_disconnected() { - if let Some(render) = self.render_disconnected_overlay.take() { - let result = render(self, cx); - self.render_disconnected_overlay = Some(render); - Some(result) + .child( + h_flex() + .flex_1() + .when_some(paddings.0, |this, p| { + this.child(p.border_r_1()) + }) + .child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )) + .when_some(paddings.1, |this, p| { + this.child(p.border_l_1()) + }), + ) + .children( + self.zoomed_position + .ne(&Some(DockPosition::Bottom)) + .then(|| self.bottom_dock.clone()), + ), + ) + // Right Dock + .children( + self.zoomed_position.ne(&Some(DockPosition::Right)).then( + || { + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.right_dock.clone()) + }, + ), + ), + ) + .children(self.zoomed.as_ref().and_then(|view| { + let zoomed_view = view.upgrade()?; + let div = div() + .occlude() + .absolute() + .overflow_hidden() + .border_color(colors.border) + .bg(colors.background) + .child(zoomed_view) + .inset_0() + .shadow_lg(); + + Some(match self.zoomed_position { + Some(DockPosition::Left) => div.right_2().border_r_1(), + Some(DockPosition::Right) => div.left_2().border_l_1(), + Some(DockPosition::Bottom) => div.top_2().border_t_1(), + None => div.top_2().bottom_2().left_2().right_2().border_1(), + }) + })) + .child(self.modal_layer.clone()) + .children(self.render_notifications(cx)), + ) + .child(self.status_bar.clone()) + .children(if self.project.read(cx).is_disconnected() { + if let Some(render) = self.render_disconnected_overlay.take() { + let result = render(self, cx); + self.render_disconnected_overlay = Some(render); + Some(result) + } else { + None + } } else { None - } - } else { - None - }) + }), + cx, + ) } } @@ -6474,3 +6482,267 @@ mod tests { }); } } + +pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful
{ + const BORDER_SIZE: Pixels = px(1.0); + let decorations = cx.window_decorations(); + + if matches!(decorations, Decorations::Client { .. }) { + cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW); + } + + struct GlobalResizeEdge(ResizeEdge); + impl Global for GlobalResizeEdge {} + + div() + .id("window-backdrop") + .bg(transparent_black()) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling, .. } => div + .child( + canvas( + |_bounds, cx| { + cx.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + cx.window_bounds().get_bounds().size, + ), + false, + ) + }, + move |_bounds, hitbox, cx| { + let mouse = cx.mouse_position(); + let size = cx.window_bounds().get_bounds().size; + let Some(edge) = resize_edge( + mouse, + theme::CLIENT_SIDE_DECORATION_SHADOW, + size, + tiling, + ) else { + return; + }; + cx.set_global(GlobalResizeEdge(edge)); + cx.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => { + CursorStyle::ResizeUpDown + } + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| { + div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW) + }) + .when(!tiling.bottom, |div| { + div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW) + }) + .when(!tiling.left, |div| { + div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW) + }) + .when(!tiling.right, |div| { + div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW) + }) + .on_mouse_move(move |e, cx| { + let size = cx.window_bounds().get_bounds().size; + let pos = e.position; + + let new_edge = + resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling); + + let edge = cx.try_global::(); + if new_edge != edge.map(|edge| edge.0) { + cx.window_handle() + .update(cx, |workspace, cx| cx.notify(workspace.entity_id())) + .ok(); + } + }) + .on_mouse_down(MouseButton::Left, move |e, cx| { + let size = cx.window_bounds().get_bounds().size; + let pos = e.position; + + let edge = match resize_edge( + pos, + theme::CLIENT_SIDE_DECORATION_SHADOW, + size, + tiling, + ) { + Some(value) => value, + None => return, + }; + + cx.start_window_resize(edge); + }), + }) + .size_full() + .child( + div() + .cursor(CursorStyle::Arrow) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .border_color(cx.theme().colors().border) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) + .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) + .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) + .when(!tiling.right, |div| div.border_r(BORDER_SIZE)) + .when(!tiling.is_tiled(), |div| { + div.shadow(smallvec::smallvec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + }]) + }), + }) + .on_mouse_move(|_e, cx| { + cx.stop_propagation(); + }) + .bg(cx.theme().colors().border) + .size_full() + .child(element), + ) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling, .. } => div.child( + canvas( + |_bounds, cx| { + cx.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + cx.window_bounds().get_bounds().size, + ), + false, + ) + }, + move |_bounds, hitbox, cx| { + let mouse = cx.mouse_position(); + let size = cx.window_bounds().get_bounds().size; + let Some(edge) = + resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling) + else { + return; + }; + cx.set_global(GlobalResizeEdge(edge)); + cx.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown, + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ), + }) +} + +fn resize_edge( + pos: Point, + shadow_size: Pixels, + window_size: Size, + tiling: Tiling, +) -> Option { + let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5); + if bounds.contains(&pos) { + return None; + } + + let corner_size = size(shadow_size * 1.5, shadow_size * 1.5); + let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size); + if top_left_bounds.contains(&pos) { + return Some(ResizeEdge::TopLeft); + } + + let top_right_bounds = Bounds::new( + Point::new(window_size.width - corner_size.width, px(0.)), + corner_size, + ); + if top_right_bounds.contains(&pos) { + return Some(ResizeEdge::TopRight); + } + + let bottom_left_bounds = Bounds::new( + Point::new(px(0.), window_size.height - corner_size.height), + corner_size, + ); + if bottom_left_bounds.contains(&pos) { + return Some(ResizeEdge::BottomLeft); + } + + let bottom_right_bounds = Bounds::new( + Point::new( + window_size.width - corner_size.width, + window_size.height - corner_size.height, + ), + corner_size, + ); + if bottom_right_bounds.contains(&pos) { + return Some(ResizeEdge::BottomRight); + } + + if !tiling.top && pos.y < shadow_size { + Some(ResizeEdge::Top) + } else if !tiling.bottom && pos.y > window_size.height - shadow_size { + Some(ResizeEdge::Bottom) + } else if !tiling.left && pos.x < shadow_size { + Some(ResizeEdge::Left) + } else if !tiling.right && pos.x > window_size.width - shadow_size { + Some(ResizeEdge::Right) + } else { + None + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 15fef998b0832f..48009666f7c7b4 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -105,6 +105,7 @@ pub fn build_window_options(display_uuid: Option, cx: &mut AppContext) -> display_id: display.map(|display| display.id()), window_background: cx.theme().window_background_appearance(), app_id: Some(app_id.to_owned()), + window_decorations: Some(gpui::WindowDecorations::Client), window_min_size: Some(gpui::Size { width: px(360.0), height: px(240.0), diff --git a/crates/zed/src/zed/linux_prompts.rs b/crates/zed/src/zed/linux_prompts.rs index fdcfbdc1242f2b..949bba49362b6f 100644 --- a/crates/zed/src/zed/linux_prompts.rs +++ b/crates/zed/src/zed/linux_prompts.rs @@ -1,7 +1,7 @@ use gpui::{ - div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, - InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, - Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext, + div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement, + IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render, + RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext, }; use settings::Settings; use theme::ThemeSettings; @@ -101,35 +101,24 @@ impl Render for FallbackPromptRenderer { }), )); - div() - .size_full() - .occlude() - .child( - div() - .size_full() - .bg(opaque_grey(0.5, 0.6)) - .absolute() - .top_0() - .left_0(), - ) - .child( - div() - .size_full() - .absolute() - .top_0() - .left_0() - .flex() - .flex_col() - .justify_around() - .child( - div() - .w_full() - .flex() - .flex_row() - .justify_around() - .child(prompt), - ), - ) + div().size_full().occlude().child( + div() + .size_full() + .absolute() + .top_0() + .left_0() + .flex() + .flex_col() + .justify_around() + .child( + div() + .w_full() + .flex() + .flex_row() + .justify_around() + .child(prompt), + ), + ) } } From 492040dec4e0df70c912894db4bdb7864508c995 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 3 Jul 2024 11:34:34 -0700 Subject: [PATCH 17/40] fix duplicated code --- crates/workspace/src/workspace.rs | 45 ------------------------------- 1 file changed, 45 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index afc494a6ea0ef1..e60fb88d7b6a4b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6500,51 +6500,6 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext .map(|div| match decorations { Decorations::Server => div, Decorations::Client { tiling, .. } => div - .child( - canvas( - |_bounds, cx| { - cx.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - cx.window_bounds().get_bounds().size, - ), - false, - ) - }, - move |_bounds, hitbox, cx| { - let mouse = cx.mouse_position(); - let size = cx.window_bounds().get_bounds().size; - let Some(edge) = resize_edge( - mouse, - theme::CLIENT_SIDE_DECORATION_SHADOW, - size, - tiling, - ) else { - return; - }; - cx.set_global(GlobalResizeEdge(edge)); - cx.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => { - CursorStyle::ResizeUpDown - } - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ) .when(!(tiling.top || tiling.right), |div| { div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) }) From 75d2e04a1dbb6a5ebc5f8e1beabf3a3fa40d78bf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 17:04:08 -0400 Subject: [PATCH 18/40] assistant: Add `/docs` slash command (#13794) This PR adds a new `/docs` slash command to the Assistant. This slash command replaces `/rustdoc`. The `/docs` slash command works with different providers. There is currently a built-in provider for rustdoc, but new providers can be defined within extensions. The Gleam extension contains an example of this. When you first type `/docs` a completion menu will be shown with the list of available providers: https://github.com/zed-industries/zed/assets/1486634/32287000-5855-44d9-a2eb-569596f5abd9 After completing the provider you want to use then you can type the package name and/or item path to search for the relevant docs: https://github.com/zed-industries/zed/assets/1486634/6fc55a63-7fcd-42ea-80ce-08c670bf03fc There are still some rough edges around completions that I would like to get cleaned up in a future PR. Both of these seem to stem from the fact that we're using an intermediate completion in the slash command: 1. Accepting a provider completion will show an error until you press Space to continue typing. - We need a way of not submitting a slash command when a completion is accepted. 2. We currently need to show the provider name in the documentation item completion list. - Without it, the provider name gets wiped out when accepting a completion, causing the slash command to become invalid. Release Notes: - N/A --- crates/assistant/src/assistant.rs | 7 +- crates/assistant/src/assistant_panel.rs | 29 +- crates/assistant/src/slash_command.rs | 2 +- .../src/slash_command/docs_command.rs | 365 ++++++++++++++++++ .../src/slash_command/rustdoc_command.rs | 265 ------------- crates/indexed_docs/src/providers/rustdoc.rs | 10 - crates/indexed_docs/src/registry.rs | 8 + crates/indexed_docs/src/store.rs | 18 +- 8 files changed, 397 insertions(+), 307 deletions(-) create mode 100644 crates/assistant/src/slash_command/docs_command.rs delete mode 100644 crates/assistant/src/slash_command/rustdoc_command.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index b938c31c099444..e97afd2c8d9d99 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -27,8 +27,9 @@ use semantic_index::{CloudEmbeddingProvider, SemanticIndex}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use slash_command::{ - active_command, default_command, diagnostics_command, fetch_command, file_command, now_command, - project_command, prompt_command, rustdoc_command, search_command, tabs_command, term_command, + active_command, default_command, diagnostics_command, docs_command, fetch_command, + file_command, now_command, project_command, prompt_command, search_command, tabs_command, + term_command, }; use std::{ fmt::{self, Display}, @@ -323,7 +324,7 @@ fn register_slash_commands(cx: &mut AppContext) { slash_command_registry.register_command(term_command::TermSlashCommand, true); slash_command_registry.register_command(now_command::NowSlashCommand, true); slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true); - slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false); + slash_command_registry.register_command(docs_command::DocsSlashCommand, true); slash_command_registry.register_command(fetch_command::FetchSlashCommand, false); } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 529c14b094e3ac..77a75feafe2881 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,3 +1,4 @@ +use crate::slash_command::docs_command::{DocsSlashCommand, DocsSlashCommandArgs}; use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, humanize_token_count, @@ -39,7 +40,7 @@ use gpui::{ Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext, }; -use indexed_docs::{IndexedDocsStore, PackageName, ProviderId}; +use indexed_docs::IndexedDocsStore; use language::{ language_settings::SoftWrap, AnchorRangeExt as _, AutoindentMode, Buffer, LanguageRegistry, LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _, @@ -2695,8 +2696,8 @@ impl ContextEditor { // TODO: In the future we should investigate how we can expose // this as a hook on the `SlashCommand` trait so that we don't // need to special-case it here. - if command.name == "rustdoc" { - return render_rustdoc_slash_command_trailer( + if command.name == DocsSlashCommand::NAME { + return render_docs_slash_command_trailer( row, command.clone(), cx, @@ -3405,25 +3406,29 @@ fn render_pending_slash_command_gutter_decoration( icon.into_any_element() } -fn render_rustdoc_slash_command_trailer( +fn render_docs_slash_command_trailer( row: MultiBufferRow, command: PendingSlashCommand, cx: &mut WindowContext, ) -> AnyElement { - let Some(rustdoc_store) = IndexedDocsStore::try_global(ProviderId::rustdoc(), cx).ok() else { + let Some(argument) = command.argument else { return Empty.into_any(); }; - let Some((crate_name, _)) = command - .argument - .as_ref() - .and_then(|arg| arg.split_once(':')) + let args = DocsSlashCommandArgs::parse(&argument); + + let Some(store) = args + .provider() + .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok()) else { return Empty.into_any(); }; - let crate_name = PackageName::from(crate_name); - if !rustdoc_store.is_indexing(&crate_name) { + let Some(package) = args.package() else { + return Empty.into_any(); + }; + + if !store.is_indexing(&package) { return Empty.into_any(); } @@ -3434,7 +3439,7 @@ fn render_rustdoc_slash_command_trailer( Animation::new(Duration::from_secs(4)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), )) - .tooltip(move |cx| Tooltip::text(format!("Indexing {crate_name}…"), cx)) + .tooltip(move |cx| Tooltip::text(format!("Indexing {package}…"), cx)) .into_any_element() } diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 5f20b08e98cb9d..37678e0fc604e0 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -20,12 +20,12 @@ use workspace::Workspace; pub mod active_command; pub mod default_command; pub mod diagnostics_command; +pub mod docs_command; pub mod fetch_command; pub mod file_command; pub mod now_command; pub mod project_command; pub mod prompt_command; -pub mod rustdoc_command; pub mod search_command; pub mod tabs_command; pub mod term_command; diff --git a/crates/assistant/src/slash_command/docs_command.rs b/crates/assistant/src/slash_command/docs_command.rs new file mode 100644 index 00000000000000..c66e68bef1d14e --- /dev/null +++ b/crates/assistant/src/slash_command/docs_command.rs @@ -0,0 +1,365 @@ +use std::path::Path; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use anyhow::{anyhow, bail, Result}; +use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; +use gpui::{AppContext, Model, Task, WeakView}; +use indexed_docs::{ + IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer, +}; +use language::LspAdapterDelegate; +use project::{Project, ProjectPath}; +use ui::prelude::*; +use util::{maybe, ResultExt}; +use workspace::Workspace; + +pub(crate) struct DocsSlashCommand; + +impl DocsSlashCommand { + pub const NAME: &'static str = "docs"; + + fn path_to_cargo_toml(project: Model, cx: &mut AppContext) -> Option> { + let worktree = project.read(cx).worktrees().next()?; + let worktree = worktree.read(cx); + let entry = worktree.entry_for_path("Cargo.toml")?; + let path = ProjectPath { + worktree_id: worktree.id(), + path: entry.path.clone(), + }; + Some(Arc::from( + project.read(cx).absolute_path(&path, cx)?.as_path(), + )) + } + + /// Ensures that the rustdoc provider is registered. + /// + /// Ideally we would do this sooner, but we need to wait until we're able to + /// access the workspace so we can read the project. + fn ensure_rustdoc_provider_is_registered( + &self, + workspace: Option>, + cx: &mut AppContext, + ) { + let indexed_docs_registry = IndexedDocsRegistry::global(cx); + if indexed_docs_registry + .get_provider_store(ProviderId::rustdoc()) + .is_none() + { + let index_provider_deps = maybe!({ + let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?; + let workspace = workspace + .upgrade() + .ok_or_else(|| anyhow!("workspace was dropped"))?; + let project = workspace.read(cx).project().clone(); + let fs = project.read(cx).fs().clone(); + let cargo_workspace_root = Self::path_to_cargo_toml(project, cx) + .and_then(|path| path.parent().map(|path| path.to_path_buf())) + .ok_or_else(|| anyhow!("no Cargo workspace root found"))?; + + anyhow::Ok((fs, cargo_workspace_root)) + }); + + if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() { + indexed_docs_registry.register_provider(Box::new(RustdocIndexer::new(Box::new( + LocalProvider::new(fs, cargo_workspace_root), + )))); + } + } + } +} + +impl SlashCommand for DocsSlashCommand { + fn name(&self) -> String { + Self::NAME.into() + } + + fn description(&self) -> String { + "insert docs".into() + } + + fn menu_text(&self) -> String { + "Insert Documentation".into() + } + + fn requires_argument(&self) -> bool { + true + } + + fn complete_argument( + self: Arc, + query: String, + _cancel: Arc, + workspace: Option>, + cx: &mut AppContext, + ) -> Task>> { + self.ensure_rustdoc_provider_is_registered(workspace, cx); + + let indexed_docs_registry = IndexedDocsRegistry::global(cx); + let args = DocsSlashCommandArgs::parse(&query); + let store = args + .provider() + .ok_or_else(|| anyhow!("no docs provider specified")) + .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); + cx.background_executor().spawn(async move { + /// HACK: Prefixes the completions with the provider ID so that it doesn't get deleted + /// when a completion is accepted. + /// + /// We will likely want to extend `complete_argument` with support for replacing just + /// a particular range of the argument when a completion is accepted. + fn prefix_with_provider(provider: ProviderId, items: Vec) -> Vec { + items + .into_iter() + .map(|item| format!("{provider} {item}")) + .collect() + } + + match args { + DocsSlashCommandArgs::NoProvider => { + let providers = indexed_docs_registry.list_providers(); + Ok(providers + .into_iter() + .map(|provider| provider.to_string()) + .collect()) + } + DocsSlashCommandArgs::SearchPackageDocs { + provider, + package, + index, + } => { + let store = store?; + + if index { + // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it + // until it completes. + let _ = store.clone().index(package.as_str().into()); + } + + let items = store.search(package).await; + Ok(prefix_with_provider(provider, items)) + } + DocsSlashCommandArgs::SearchItemDocs { + provider, + item_path, + .. + } => { + let store = store?; + let items = store.search(item_path).await; + Ok(prefix_with_provider(provider, items)) + } + } + }) + } + + fn run( + self: Arc, + argument: Option<&str>, + _workspace: WeakView, + _delegate: Arc, + cx: &mut WindowContext, + ) -> Task> { + let Some(argument) = argument else { + return Task::ready(Err(anyhow!("missing argument"))); + }; + + let args = DocsSlashCommandArgs::parse(argument); + let text = cx.background_executor().spawn({ + let store = args + .provider() + .ok_or_else(|| anyhow!("no docs provider specified")) + .and_then(|provider| IndexedDocsStore::try_global(provider, cx)); + async move { + match args { + DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"), + DocsSlashCommandArgs::SearchPackageDocs { + provider, package, .. + } => { + let store = store?; + let item_docs = store.load(package.clone()).await?; + + anyhow::Ok((provider, package, item_docs.to_string())) + } + DocsSlashCommandArgs::SearchItemDocs { + provider, + item_path, + .. + } => { + let store = store?; + let item_docs = store.load(item_path.clone()).await?; + + anyhow::Ok((provider, item_path, item_docs.to_string())) + } + } + } + }); + + cx.foreground_executor().spawn(async move { + let (provider, path, text) = text.await?; + let range = 0..text.len(); + Ok(SlashCommandOutput { + text, + sections: vec![SlashCommandOutputSection { + range, + icon: IconName::FileRust, + label: format!("docs ({provider}): {path}",).into(), + }], + run_commands_in_text: false, + }) + }) + } +} + +fn is_item_path_delimiter(char: char) -> bool { + !char.is_alphanumeric() && char != '-' && char != '_' +} + +#[derive(Debug, PartialEq)] +pub(crate) enum DocsSlashCommandArgs { + NoProvider, + SearchPackageDocs { + provider: ProviderId, + package: String, + index: bool, + }, + SearchItemDocs { + provider: ProviderId, + package: String, + item_path: String, + }, +} + +impl DocsSlashCommandArgs { + pub fn parse(argument: &str) -> Self { + let Some((provider, argument)) = argument.split_once(' ') else { + return Self::NoProvider; + }; + + let provider = ProviderId(provider.into()); + + if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) { + if rest.trim().is_empty() { + Self::SearchPackageDocs { + provider, + package: package.to_owned(), + index: true, + } + } else { + Self::SearchItemDocs { + provider, + package: package.to_owned(), + item_path: argument.to_owned(), + } + } + } else { + Self::SearchPackageDocs { + provider, + package: argument.to_owned(), + index: false, + } + } + } + + pub fn provider(&self) -> Option { + match self { + Self::NoProvider => None, + Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => { + Some(provider.clone()) + } + } + } + + pub fn package(&self) -> Option { + match self { + Self::NoProvider => None, + Self::SearchPackageDocs { package, .. } | Self::SearchItemDocs { package, .. } => { + Some(package.as_str().into()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_docs_slash_command_args() { + assert_eq!( + DocsSlashCommandArgs::parse(""), + DocsSlashCommandArgs::NoProvider + ); + assert_eq!( + DocsSlashCommandArgs::parse("rustdoc"), + DocsSlashCommandArgs::NoProvider + ); + + assert_eq!( + DocsSlashCommandArgs::parse("rustdoc "), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("rustdoc".into()), + package: "".into(), + index: false + } + ); + assert_eq!( + DocsSlashCommandArgs::parse("gleam "), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("gleam".into()), + package: "".into(), + index: false + } + ); + + assert_eq!( + DocsSlashCommandArgs::parse("rustdoc gpui"), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("rustdoc".into()), + package: "gpui".into(), + index: false, + } + ); + assert_eq!( + DocsSlashCommandArgs::parse("gleam gleam_stdlib"), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("gleam".into()), + package: "gleam_stdlib".into(), + index: false + } + ); + + // Adding an item path delimiter indicates we can start indexing. + assert_eq!( + DocsSlashCommandArgs::parse("rustdoc gpui:"), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("rustdoc".into()), + package: "gpui".into(), + index: true, + } + ); + assert_eq!( + DocsSlashCommandArgs::parse("gleam gleam_stdlib/"), + DocsSlashCommandArgs::SearchPackageDocs { + provider: ProviderId("gleam".into()), + package: "gleam_stdlib".into(), + index: true + } + ); + + assert_eq!( + DocsSlashCommandArgs::parse("rustdoc gpui::foo::bar::Baz"), + DocsSlashCommandArgs::SearchItemDocs { + provider: ProviderId("rustdoc".into()), + package: "gpui".into(), + item_path: "gpui::foo::bar::Baz".into() + } + ); + assert_eq!( + DocsSlashCommandArgs::parse("gleam gleam_stdlib/gleam/int"), + DocsSlashCommandArgs::SearchItemDocs { + provider: ProviderId("gleam".into()), + package: "gleam_stdlib".into(), + item_path: "gleam_stdlib/gleam/int".into() + } + ); + } +} diff --git a/crates/assistant/src/slash_command/rustdoc_command.rs b/crates/assistant/src/slash_command/rustdoc_command.rs deleted file mode 100644 index 5f5c7fac84732b..00000000000000 --- a/crates/assistant/src/slash_command/rustdoc_command.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::path::Path; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; - -use anyhow::{anyhow, bail, Context, Result}; -use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; -use fs::Fs; -use futures::AsyncReadExt; -use gpui::{AppContext, Model, Task, WeakView}; -use http::{AsyncBody, HttpClient, HttpClientWithUrl}; -use indexed_docs::{ - convert_rustdoc_to_markdown, IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, - ProviderId, RustdocIndexer, RustdocSource, -}; -use language::LspAdapterDelegate; -use project::{Project, ProjectPath}; -use ui::prelude::*; -use util::{maybe, ResultExt}; -use workspace::Workspace; - -pub(crate) struct RustdocSlashCommand; - -impl RustdocSlashCommand { - async fn build_message( - fs: Arc, - http_client: Arc, - crate_name: PackageName, - module_path: Vec, - path_to_cargo_toml: Option<&Path>, - ) -> Result<(RustdocSource, String)> { - let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent()); - if let Some(cargo_workspace_root) = cargo_workspace_root { - let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc"); - local_cargo_doc_path.push(crate_name.as_ref()); - if !module_path.is_empty() { - local_cargo_doc_path.push(module_path.join("/")); - } - local_cargo_doc_path.push("index.html"); - - if let Ok(contents) = fs.load(&local_cargo_doc_path).await { - let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?; - - return Ok((RustdocSource::Local, markdown)); - } - } - - let version = "latest"; - let path = format!( - "{crate_name}/{version}/{crate_name}/{module_path}", - module_path = module_path.join("/") - ); - - let mut response = http_client - .get( - &format!("https://docs.rs/{path}"), - AsyncBody::default(), - true, - ) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading docs.rs response body")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?; - - Ok((RustdocSource::DocsDotRs, markdown)) - } - - fn path_to_cargo_toml(project: Model, cx: &mut AppContext) -> Option> { - let worktree = project.read(cx).worktrees().next()?; - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path("Cargo.toml")?; - let path = ProjectPath { - worktree_id: worktree.id(), - path: entry.path.clone(), - }; - Some(Arc::from( - project.read(cx).absolute_path(&path, cx)?.as_path(), - )) - } - - /// Ensures that the rustdoc provider is registered. - /// - /// Ideally we would do this sooner, but we need to wait until we're able to - /// access the workspace so we can read the project. - fn ensure_rustdoc_provider_is_registered( - &self, - workspace: Option>, - cx: &mut AppContext, - ) { - let indexed_docs_registry = IndexedDocsRegistry::global(cx); - if indexed_docs_registry - .get_provider_store(ProviderId::rustdoc()) - .is_none() - { - let index_provider_deps = maybe!({ - let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?; - let workspace = workspace - .upgrade() - .ok_or_else(|| anyhow!("workspace was dropped"))?; - let project = workspace.read(cx).project().clone(); - let fs = project.read(cx).fs().clone(); - let cargo_workspace_root = Self::path_to_cargo_toml(project, cx) - .and_then(|path| path.parent().map(|path| path.to_path_buf())) - .ok_or_else(|| anyhow!("no Cargo workspace root found"))?; - - anyhow::Ok((fs, cargo_workspace_root)) - }); - - if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() { - indexed_docs_registry.register_provider(Box::new(RustdocIndexer::new(Box::new( - LocalProvider::new(fs, cargo_workspace_root), - )))); - } - } - } -} - -impl SlashCommand for RustdocSlashCommand { - fn name(&self) -> String { - "rustdoc".into() - } - - fn description(&self) -> String { - "insert Rust docs".into() - } - - fn menu_text(&self) -> String { - "Insert Rust Documentation".into() - } - - fn requires_argument(&self) -> bool { - true - } - - fn complete_argument( - self: Arc, - query: String, - _cancel: Arc, - workspace: Option>, - cx: &mut AppContext, - ) -> Task>> { - self.ensure_rustdoc_provider_is_registered(workspace, cx); - - let store = IndexedDocsStore::try_global(ProviderId::rustdoc(), cx); - cx.background_executor().spawn(async move { - let store = store?; - - if let Some((crate_name, rest)) = query.split_once(':') { - if rest.is_empty() { - // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it - // until it completes. - let _ = store.clone().index(crate_name.into()); - } - } - - let items = store.search(query).await; - Ok(items) - }) - } - - fn run( - self: Arc, - argument: Option<&str>, - workspace: WeakView, - _delegate: Arc, - cx: &mut WindowContext, - ) -> Task> { - let Some(argument) = argument else { - return Task::ready(Err(anyhow!("missing crate name"))); - }; - let Some(workspace) = workspace.upgrade() else { - return Task::ready(Err(anyhow!("workspace was dropped"))); - }; - - let project = workspace.read(cx).project().clone(); - let fs = project.read(cx).fs().clone(); - let http_client = workspace.read(cx).client().http_client(); - let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx); - - let mut path_components = argument.split("::"); - let crate_name = match path_components - .next() - .ok_or_else(|| anyhow!("missing crate name")) - { - Ok(crate_name) => PackageName::from(crate_name), - Err(err) => return Task::ready(Err(err)), - }; - let item_path = path_components.map(ToString::to_string).collect::>(); - - let text = cx.background_executor().spawn({ - let rustdoc_store = IndexedDocsStore::try_global(ProviderId::rustdoc(), cx); - let crate_name = crate_name.clone(); - let item_path = item_path.clone(); - async move { - let rustdoc_store = rustdoc_store?; - let item_docs = rustdoc_store - .load( - crate_name.clone(), - if item_path.is_empty() { - None - } else { - Some(item_path.join("::")) - }, - ) - .await; - - if let Ok(item_docs) = item_docs { - anyhow::Ok((RustdocSource::Index, item_docs.to_string())) - } else { - Self::build_message( - fs, - http_client, - crate_name, - item_path, - path_to_cargo_toml.as_deref(), - ) - .await - } - } - }); - - let module_path = if item_path.is_empty() { - None - } else { - Some(SharedString::from(item_path.join("::"))) - }; - cx.foreground_executor().spawn(async move { - let (source, text) = text.await?; - let range = 0..text.len(); - let crate_path = module_path - .map(|module_path| format!("{}::{}", crate_name, module_path)) - .unwrap_or_else(|| crate_name.to_string()); - Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - icon: IconName::FileRust, - label: format!( - "rustdoc ({source}): {crate_path}", - source = match source { - RustdocSource::Index => "index", - RustdocSource::Local => "local", - RustdocSource::DocsDotRs => "docs.rs", - } - ) - .into(), - }], - run_commands_in_text: false, - }) - }) - } -} diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs index 7dae48da3f9294..9b3038a2e56851 100644 --- a/crates/indexed_docs/src/providers/rustdoc.rs +++ b/crates/indexed_docs/src/providers/rustdoc.rs @@ -16,16 +16,6 @@ use http::{AsyncBody, HttpClient, HttpClientWithUrl}; use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; -#[derive(Debug, Clone, Copy)] -pub enum RustdocSource { - /// The docs were sourced from Zed's rustdoc index. - Index, - /// The docs were sourced from local `cargo doc` output. - Local, - /// The docs were sourced from `docs.rs`. - DocsDotRs, -} - #[derive(Debug)] struct RustdocItemWithHistory { pub item: RustdocItem, diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs index e251b3c616de35..fa3425466c3bf0 100644 --- a/crates/indexed_docs/src/registry.rs +++ b/crates/indexed_docs/src/registry.rs @@ -34,6 +34,14 @@ impl IndexedDocsRegistry { } } + pub fn list_providers(&self) -> Vec { + self.stores_by_provider + .read() + .keys() + .cloned() + .collect::>() + } + pub fn register_provider( &self, provider: Box, diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs index 5c53c128d8e172..438cd19b69f563 100644 --- a/crates/indexed_docs/src/store.rs +++ b/crates/indexed_docs/src/store.rs @@ -94,22 +94,12 @@ impl IndexedDocsStore { self.indexing_tasks_by_package.read().contains_key(package) } - pub async fn load( - &self, - package: PackageName, - item_path: Option, - ) -> Result { - let item_path = if let Some(item_path) = item_path { - format!("{package}::{item_path}") - } else { - package.to_string() - }; - + pub async fn load(&self, key: String) -> Result { self.database_future .clone() .await .map_err(|err| anyhow!(err))? - .load(item_path) + .load(key) .await } @@ -160,10 +150,6 @@ impl IndexedDocsStore { let executor = self.executor.clone(); let database_future = self.database_future.clone(); self.executor.spawn(async move { - if query.is_empty() { - return Vec::new(); - } - let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { return Vec::new(); }; From 05af87e416c560ae4bfce76795ae18b232ba5cf6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 17:17:02 -0400 Subject: [PATCH 19/40] Rename `DiagnosticsCommand` to `DiagnosticsSlashCommand` (#13795) This PR renames the `DiagnosticsCommand` to `DiagnosticsSlashCommand` to match the rest of our slash commands. Release Notes: - N/A --- crates/assistant/src/assistant.rs | 2 +- crates/assistant/src/slash_command/diagnostics_command.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index e97afd2c8d9d99..82defbbd6f591f 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -323,7 +323,7 @@ fn register_slash_commands(cx: &mut AppContext) { slash_command_registry.register_command(default_command::DefaultSlashCommand, true); slash_command_registry.register_command(term_command::TermSlashCommand, true); slash_command_registry.register_command(now_command::NowSlashCommand, true); - slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true); + slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true); slash_command_registry.register_command(docs_command::DocsSlashCommand, true); slash_command_registry.register_command(fetch_command::FetchSlashCommand, false); } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index 5ea47a81d1cc50..ab01b3c8031cf5 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -20,9 +20,9 @@ use util::paths::PathMatcher; use util::ResultExt; use workspace::Workspace; -pub(crate) struct DiagnosticsCommand; +pub(crate) struct DiagnosticsSlashCommand; -impl DiagnosticsCommand { +impl DiagnosticsSlashCommand { fn search_paths( &self, query: String, @@ -81,7 +81,7 @@ impl DiagnosticsCommand { } } -impl SlashCommand for DiagnosticsCommand { +impl SlashCommand for DiagnosticsSlashCommand { fn name(&self) -> String { "diagnostics".into() } From 6d10b16f79bed9c13427f93a86d56c23bf658631 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 17:56:01 -0400 Subject: [PATCH 20/40] gleam: Include a package name suffix for docs entries (#13798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the Gleam docs provider to include the package name as a suffix for docs entries: Screenshot 2024-07-03 at 5 48 28 PM This will help disambiguate modules with the same names from different packages, as well as help out with providing better completions when the package name and top-level module name do not match. Release Notes: - N/A --- extensions/gleam/src/hexdocs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/gleam/src/hexdocs.rs b/extensions/gleam/src/hexdocs.rs index cc21746934cc14..26198fc5d160d7 100644 --- a/extensions/gleam/src/hexdocs.rs +++ b/extensions/gleam/src/hexdocs.rs @@ -28,7 +28,7 @@ pub fn index(package: String, database: &KeyValueStore) -> Result<()> { let (markdown, _modules) = convert_hexdocs_to_markdown(response.body.as_bytes())?; - database.insert(&module, &markdown)?; + database.insert(&format!("{module} ({package})"), &markdown)?; } Ok(()) From 8ec478cbcd48874861574d92d499646b55ffa85d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 4 Jul 2024 00:04:28 +0200 Subject: [PATCH 21/40] Rust: Prefer completion.label_details over completion.details (#13797) In doing so we get to surface origin packages more prominently. Fixes #13494 (again) Release Notes: - Fixed origin packages not being surfaced in Rust completions --- crates/languages/src/rust.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index a92021287c0e63..6ad38e98291f49 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -203,12 +203,10 @@ impl LspAdapter for RustLspAdapter { language: &Arc, ) -> Option { let detail = completion - .detail + .label_details .as_ref() - .or(completion - .label_details - .as_ref() - .and_then(|detail| detail.detail.as_ref())) + .and_then(|detail| detail.detail.as_ref()) + .or(completion.detail.as_ref()) .map(ToOwned::to_owned); match completion.kind { Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => { From 52583fe1eda4eb748fe0d2fc21b86cf7bb4582ef Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 3 Jul 2024 19:03:49 -0400 Subject: [PATCH 22/40] Remove unused `ids` query parameter from `GET /extensions` endpoint (#13802) This PR removes the `ids` query parameter from the `GET /extensions` endpoint, as we don't use it. We originally added the query parameter in #9929 to facilitate auto-updates. However, it was superseded by the `GET /extensions/updates` endpoint in #10052. There shouldn't be any Zed versions out in the wild that are using the `ids` query parameter, as we added the endpoint on Thursday, March 28, and replaced its usage with the new endpoint on Monday, April 1, before the next Zed release. Release Notes: - N/A --- crates/collab/src/api/extensions.rs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/crates/collab/src/api/extensions.rs b/crates/collab/src/api/extensions.rs index 4f797adb689218..ed70b347a9c2e0 100644 --- a/crates/collab/src/api/extensions.rs +++ b/crates/collab/src/api/extensions.rs @@ -36,8 +36,6 @@ pub fn router() -> Router { struct GetExtensionsParams { filter: Option, #[serde(default)] - ids: Option, - #[serde(default)] max_schema_version: i32, } @@ -45,26 +43,15 @@ async fn get_extensions( Extension(app): Extension>, Query(params): Query, ) -> Result> { - let extension_ids = params - .ids - .as_ref() - .map(|s| s.split(',').map(|s| s.trim()).collect::>()); - - let extensions = if let Some(extension_ids) = extension_ids { - app.db.get_extensions_by_ids(&extension_ids, None).await? - } else { - let result = app - .db - .get_extensions(params.filter.as_deref(), params.max_schema_version, 500) - .await?; - - if let Some(query) = params.filter.as_deref() { - let count = result.len(); - tracing::info!(query, count, "extension_search") - } + let extensions = app + .db + .get_extensions(params.filter.as_deref(), params.max_schema_version, 500) + .await?; - result - }; + if let Some(query) = params.filter.as_deref() { + let count = extensions.len(); + tracing::info!(query, count, "extension_search") + } Ok(Json(GetExtensionsResponse { data: extensions })) } From ed09bb949c2be7972234fe9cbb06def686182860 Mon Sep 17 00:00:00 2001 From: Yongkang Chen Date: Wed, 3 Jul 2024 17:38:19 -0700 Subject: [PATCH 23/40] Fix panel state (#13668) In the latest update, panel loading occasionally occurred randomly, either before or after workspace deserialization due to their asynchronous nature. This update addresses the issue by ensuring panels restore their state based on serialized data, synchronizing their loading with workspace deserialization. Release Notes: - Fixed [#9638](https://github.com/zed-industries/zed/issues/9638) - Fixed [#12954](https://github.com/zed-industries/zed/issues/12954) --- crates/workspace/src/dock.rs | 36 ++++++++++++++++++------------- crates/workspace/src/workspace.rs | 24 ++++++++++----------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index d84aace8f92799..f9fb7c88011d35 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -431,25 +431,12 @@ impl Dock { }), ]; - let name = panel.persistent_name().to_string(); - self.panel_entries.push(PanelEntry { panel: Arc::new(panel.clone()), _subscriptions: subscriptions, }); - if let Some(serialized) = self.serialized_dock.clone() { - if serialized.active_panel == Some(name) { - self.activate_panel(self.panel_entries.len() - 1, cx); - if serialized.visible { - self.set_open(true, cx); - } - if serialized.zoom { - if let Some(panel) = self.active_panel() { - panel.set_zoomed(true, cx) - }; - } - } - } else if panel.read(cx).starts_open(cx) { + + if !self.restore_state(cx) && panel.read(cx).starts_open(cx) { self.activate_panel(self.panel_entries.len() - 1, cx); self.set_open(true, cx); } @@ -457,6 +444,25 @@ impl Dock { cx.notify() } + pub fn restore_state(&mut self, cx: &mut ViewContext) -> bool { + if let Some(serialized) = self.serialized_dock.clone() { + if let Some(active_panel) = serialized.active_panel { + if let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) { + self.activate_panel(idx, cx); + } + } + + if serialized.zoom { + if let Some(panel) = self.active_panel() { + panel.set_zoomed(true, cx) + } + } + self.set_open(serialized.visible, cx); + return true; + } + return false; + } + pub fn remove_panel(&mut self, panel: &View, cx: &mut ViewContext) { if let Some(panel_ix) = self .panel_entries diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e60fb88d7b6a4b..ba8476fbb4aaa9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3813,18 +3813,18 @@ impl Workspace { let docks = serialized_workspace.docks; - let right = docks.right.clone(); - workspace - .right_dock - .update(cx, |dock, _| dock.serialized_dock = Some(right)); - let left = docks.left.clone(); - workspace - .left_dock - .update(cx, |dock, _| dock.serialized_dock = Some(left)); - let bottom = docks.bottom.clone(); - workspace - .bottom_dock - .update(cx, |dock, _| dock.serialized_dock = Some(bottom)); + for (dock, serialized_dock) in [ + (&mut workspace.right_dock, docks.right), + (&mut workspace.left_dock, docks.left), + (&mut workspace.bottom_dock, docks.bottom), + ] + .iter_mut() + { + dock.update(cx, |dock, cx| { + dock.serialized_dock = Some(serialized_dock.clone()); + dock.restore_state(cx); + }); + } cx.notify(); })?; From 818e6e53d625378a00ae3418a3991cdd2c78d249 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 4 Jul 2024 10:11:24 +0200 Subject: [PATCH 24/40] Introduce Tabs to Assistant Panel (#13783) image Release Notes: - N/A --- Cargo.lock | 2 +- crates/assistant/Cargo.toml | 2 +- crates/assistant/src/assistant.rs | 3 +- crates/assistant/src/assistant_panel.rs | 1163 +++++++++++++---------- crates/assistant/src/model_selector.rs | 2 +- 5 files changed, 663 insertions(+), 509 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 87df412855b2c8..ae1fa6a15266eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,6 +374,7 @@ dependencies = [ "anyhow", "assistant_slash_command", "async-watch", + "breadcrumbs", "cargo_toml", "chrono", "client", @@ -383,7 +384,6 @@ dependencies = [ "editor", "env_logger", "feature_flags", - "file_icons", "fs", "futures 0.3.28", "fuzzy", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 97f308e084af17..32c3eda6834fa6 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -17,6 +17,7 @@ anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true assistant_slash_command.workspace = true async-watch.workspace = true +breadcrumbs.workspace = true cargo_toml.workspace = true chrono.workspace = true client.workspace = true @@ -24,7 +25,6 @@ collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true -file_icons.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 82defbbd6f591f..9d2e8c01426f2b 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -49,7 +49,8 @@ actions!( ResetKey, InlineAssist, InsertActivePrompt, - ToggleHistory, + DeployHistory, + DeployPromptLibrary, ApplyEdit, ConfirmCommand, ToggleModelSelector diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 77a75feafe2881..f097338e01fb64 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -10,13 +10,14 @@ use crate::{ }, terminal_inline_assistant::TerminalInlineAssistant, ApplyEdit, Assist, CompletionProvider, ConfirmCommand, ContextStore, CycleMessageRole, - InlineAssist, InlineAssistant, InsertIntoEditor, LanguageModelRequest, - LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, ModelSelector, - QuoteSelection, ResetKey, Role, SavedContext, SavedContextMetadata, SavedMessage, Split, - ToggleFocus, ToggleHistory, ToggleModelSelector, + DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistant, InsertIntoEditor, + LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, + ModelSelector, QuoteSelection, ResetKey, Role, SavedContext, SavedContextMetadata, + SavedMessage, Split, ToggleFocus, ToggleModelSelector, }; use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; +use breadcrumbs::Breadcrumbs; use client::telemetry::Telemetry; use collections::{BTreeSet, HashMap, HashSet}; use editor::{ @@ -28,17 +29,15 @@ use editor::{ Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint, }; use editor::{display_map::CreaseId, FoldPlaceholder}; -use file_icons::FileIcons; use fs::Fs; use futures::future::Shared; use futures::{FutureExt, StreamExt}; use gpui::{ - div, percentage, point, rems, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext, - AsyncAppContext, AsyncWindowContext, ClipboardItem, Context as _, Empty, EventEmitter, - FocusHandle, FocusOutEvent, FocusableView, InteractiveElement, IntoElement, Model, - ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, - Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, - WindowContext, + div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext, + AsyncAppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, + EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, + ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, + Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext, }; use indexed_docs::IndexedDocsStore; use language::{ @@ -62,18 +61,21 @@ use std::{ }; use telemetry_events::AssistantKind; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; +use theme::ThemeSettings; use ui::{ prelude::*, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tab, TabBar, Tooltip, + ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, }; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; -use workspace::NewFile; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - searchable::Direction, - Save, ToggleZoom, Toolbar, Workspace, + item::{BreadcrumbText, Item, ItemHandle}, + pane, + searchable::{SearchEvent, SearchableItem}, + Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; +use workspace::{searchable::SearchableItemHandle, NewFile}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -100,23 +102,18 @@ pub enum AssistantPanelEvent { } pub struct AssistantPanel { + pane: View, workspace: WeakView, width: Option, height: Option, - active_context_editor: Option, - show_saved_contexts: bool, context_store: Model, - saved_context_picker: View>, - zoomed: bool, - focus_handle: FocusHandle, - toolbar: View, languages: Arc, slash_commands: Arc, fs: Arc, telemetry: Arc, - _subscriptions: Vec, + subscriptions: Vec, authentication_prompt: Option, - model_menu_handle: PopoverMenuHandle, + model_selector_menu_handle: PopoverMenuHandle, } struct SavedContextPickerDelegate { @@ -216,109 +213,137 @@ impl PickerDelegate for SavedContextPickerDelegate { } } -struct ActiveContextEditor { - editor: View, - _subscriptions: Vec, -} - impl AssistantPanel { pub fn load( workspace: WeakView, cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { + // TODO: deserialize state. let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; let context_store = cx.update(|cx| ContextStore::new(fs.clone(), cx))?.await?; - - // TODO: deserialize state. - let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { - cx.new_view::(|cx| { - let toolbar = cx.new_view(|cx| { - let mut toolbar = Toolbar::new(); - toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.new_view(BufferSearchBar::new), cx); - toolbar - }); - - let saved_context_picker = cx.new_view(|cx| { - Picker::uniform_list( - SavedContextPickerDelegate::new(context_store.clone()), - cx, - ) - .modal(false) - .max_height(None) - }); - - let focus_handle = cx.focus_handle(); - let subscriptions = vec![ - cx.on_focus_in(&focus_handle, Self::focus_in), - cx.on_focus_out(&focus_handle, Self::focus_out), - cx.observe_global::({ - let mut prev_settings_version = - CompletionProvider::global(cx).settings_version(); - move |this, cx| { - this.completion_provider_changed(prev_settings_version, cx); - prev_settings_version = - CompletionProvider::global(cx).settings_version(); - } - }), - cx.observe(&context_store, |this, _, cx| { - this.saved_context_picker - .update(cx, |picker, cx| picker.refresh(cx)); - }), - cx.subscribe( - &saved_context_picker, - Self::handle_saved_context_picker_event, - ), - ]; - - cx.observe_global::(|_, cx| { - cx.notify(); - }) - .detach(); - - Self { - workspace: workspace_handle, - active_context_editor: None, - show_saved_contexts: false, - saved_context_picker, - context_store, - zoomed: false, - focus_handle, - toolbar, - languages: workspace.app_state().languages.clone(), - slash_commands: SlashCommandRegistry::global(cx), - fs: workspace.app_state().fs.clone(), - telemetry: workspace.client().telemetry().clone(), - width: None, - height: None, - _subscriptions: subscriptions, - authentication_prompt: None, - model_menu_handle: PopoverMenuHandle::default(), - } - }) + cx.new_view(|cx| Self::new(workspace, context_store.clone(), cx)) }) }) } - fn focus_in(&mut self, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); - cx.notify(); - if self.focus_handle.is_focused(cx) { - if self.show_saved_contexts { - cx.focus_view(&self.saved_context_picker); - } else if let Some(context) = self.active_context_editor() { - cx.focus_view(context); - } + fn new( + workspace: &Workspace, + context_store: Model, + cx: &mut ViewContext, + ) -> Self { + let model_selector_menu_handle = PopoverMenuHandle::default(); + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.project().clone(), + Default::default(), + None, + NewFile.boxed_clone(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(true, cx); + pane.display_nav_history_buttons(None); + pane.set_should_display_tab_bar(|_| true); + pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + h_flex() + .gap(Spacing::Small.rems(cx)) + .child( + IconButton::new("menu", IconName::Menu) + .icon_size(IconSize::Small) + .on_click(cx.listener(|pane, _, cx| { + let zoom_label = if pane.is_zoomed() { + "Zoom Out" + } else { + "Zoom In" + }; + let menu = ContextMenu::build(cx, |menu, cx| { + menu.context(pane.focus_handle(cx)) + .action("New Context", Box::new(NewFile)) + .action("History", Box::new(DeployHistory)) + .action("Prompt Library", Box::new(DeployPromptLibrary)) + .action(zoom_label, Box::new(ToggleZoom)) + }); + cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| { + pane.new_item_menu = None; + }) + .detach(); + pane.new_item_menu = Some(menu); + })), + ) + .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Pane::render_menu_overlay(new_item_menu)) + }) + .into_any_element() + }); + pane.toolbar().update(cx, |toolbar, cx| { + toolbar.add_item(cx.new_view(|_| Breadcrumbs::new()), cx); + toolbar.add_item( + cx.new_view(|_| { + ContextEditorToolbarItem::new(workspace, model_selector_menu_handle.clone()) + }), + cx, + ); + toolbar.add_item(cx.new_view(BufferSearchBar::new), cx) + }); + pane + }); + + let subscriptions = vec![ + cx.observe(&pane, |_, _, cx| cx.notify()), + cx.subscribe(&pane, Self::handle_pane_event), + cx.observe_global::({ + let mut prev_settings_version = CompletionProvider::global(cx).settings_version(); + move |this, cx| { + this.completion_provider_changed(prev_settings_version, cx); + prev_settings_version = CompletionProvider::global(cx).settings_version(); + } + }), + ]; + + Self { + pane, + workspace: workspace.weak_handle(), + width: None, + height: None, + context_store, + languages: workspace.app_state().languages.clone(), + slash_commands: SlashCommandRegistry::global(cx), + fs: workspace.app_state().fs.clone(), + telemetry: workspace.client().telemetry().clone(), + subscriptions, + authentication_prompt: None, + model_selector_menu_handle, } } - fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); - cx.notify(); + fn handle_pane_event( + &mut self, + _pane: View, + event: &pane::Event, + cx: &mut ViewContext, + ) { + match event { + pane::Event::Remove => cx.emit(PanelEvent::Close), + pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), + pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + + pane::Event::AddItem { item } => { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + item.added_to_pane(workspace, self.pane.clone(), cx) + }); + } + } + + pane::Event::RemoveItem { .. } | pane::Event::ActivateItem { .. } => { + cx.emit(AssistantPanelEvent::ContextEdited); + } + + _ => {} + } } fn completion_provider_changed( @@ -329,7 +354,7 @@ impl AssistantPanel { if self.is_authenticated(cx) { self.authentication_prompt = None; - if let Some(editor) = self.active_context_editor() { + if let Some(editor) = self.active_context_editor(cx) { editor.update(cx, |active_context, cx| { active_context .context @@ -337,7 +362,7 @@ impl AssistantPanel { }) } - if self.active_context_editor().is_none() { + if self.active_context_editor(cx).is_none() { self.new_context(cx); } cx.notify(); @@ -352,19 +377,6 @@ impl AssistantPanel { } } - fn handle_saved_context_picker_event( - &mut self, - _picker: View>, - event: &SavedContextPickerEvent, - cx: &mut ViewContext, - ) { - match event { - SavedContextPickerEvent::Confirmed { path } => { - self.open_context(path.clone(), cx).detach_and_log_err(cx); - } - } - } - pub fn inline_assist( workspace: &mut Workspace, _: &InlineAssist, @@ -481,17 +493,18 @@ impl AssistantPanel { } } } - let context_editor = assistant_panel - .read(cx) - .active_context_editor() - .and_then(|editor| { - let editor = &editor.read(cx).editor; - if editor.read(cx).is_focused(cx) { - Some(editor.clone()) - } else { - None - } - }); + let context_editor = + assistant_panel + .read(cx) + .active_context_editor(cx) + .and_then(|editor| { + let editor = &editor.read(cx).editor; + if editor.read(cx).is_focused(cx) { + Some(editor.clone()) + } else { + None + } + }); if let Some(context_editor) = context_editor { Some(InlineAssistTarget::Editor(context_editor, false)) @@ -523,24 +536,17 @@ impl AssistantPanel { } fn show_context(&mut self, context_editor: View, cx: &mut ViewContext) { - let mut subscriptions = Vec::new(); - subscriptions.push(cx.subscribe(&context_editor, Self::handle_context_editor_event)); - - let context = context_editor.read(cx).context.clone(); - subscriptions.push(cx.observe(&context, |_, _, cx| cx.notify())); - - let editor = context_editor.read(cx).editor.clone(); - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(Some(&editor), cx); + let focus = self.focus_handle(cx).contains_focused(cx); + let prev_len = self.pane.read(cx).items_len(); + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx) }); - if self.focus_handle.contains_focused(cx) { - cx.focus_view(&editor); + + if prev_len != self.pane.read(cx).items_len() { + self.subscriptions + .push(cx.subscribe(&context_editor, Self::handle_context_editor_event)); } - self.active_context_editor = Some(ActiveContextEditor { - editor: context_editor, - _subscriptions: subscriptions, - }); - self.show_saved_contexts = false; + cx.emit(AssistantPanelEvent::ContextEdited); cx.notify(); } @@ -557,80 +563,30 @@ impl AssistantPanel { } } - fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { - if self.zoomed { - cx.emit(PanelEvent::ZoomOut) - } else { - cx.emit(PanelEvent::ZoomIn) - } - } + fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext) { + let history_item_ix = self + .pane + .read(cx) + .items() + .position(|item| item.downcast::().is_some()); - fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext) { - if self.show_saved_contexts { - self.hide_history(cx); + if let Some(history_item_ix) = history_item_ix { + self.pane.update(cx, |pane, cx| { + pane.activate_item(history_item_ix, true, true, cx); + }); } else { - self.show_history(cx); - } - } - - fn show_history(&mut self, cx: &mut ViewContext) { - cx.focus_view(&self.saved_context_picker); - if !self.show_saved_contexts { - self.show_saved_contexts = true; - cx.notify(); - } - } - - fn hide_history(&mut self, cx: &mut ViewContext) { - if let Some(editor) = self.active_context_editor() { - cx.focus_view(&editor); - self.show_saved_contexts = false; - cx.notify(); - } - } - - fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate = true; - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(cx) { - search_bar.search_suggested(cx); - if action.focus { - let focus_handle = search_bar.focus_handle(cx); - search_bar.select_query(cx); - cx.focus(&focus_handle); - } - propagate = false - } + let assistant_panel = cx.view().downgrade(); + let history = cx.new_view(|cx| { + ContextHistory::new(self.context_store.clone(), assistant_panel, cx) + }); + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(history), true, true, None, cx); }); - } - if propagate { - cx.propagate(); - } - } - - fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - if !search_bar.read(cx).is_dismissed() { - search_bar.update(cx, |search_bar, cx| { - search_bar.dismiss(&Default::default(), cx) - }); - return; - } - } - cx.propagate(); - } - - fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); } } - fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); - } + fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext) { + open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx); } fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { @@ -640,143 +596,29 @@ impl AssistantPanel { } fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { - self.model_menu_handle.toggle(cx); + self.model_selector_menu_handle.toggle(cx); } - fn insert_command(&mut self, name: &str, cx: &mut ViewContext) { - if let Some(context_editor) = self.active_context_editor() { - context_editor.update(cx, |context_editor, cx| { - context_editor.insert_command(name, cx) - }); - } - } - - fn active_context_editor(&self) -> Option<&View> { - Some(&self.active_context_editor.as_ref()?.editor) + fn active_context_editor(&self, cx: &AppContext) -> Option> { + self.pane + .read(cx) + .active_item()? + .downcast::() } pub fn active_context(&self, cx: &AppContext) -> Option> { - Some(self.active_context_editor()?.read(cx).context.clone()) - } - - fn render_popover_button(&self, cx: &mut ViewContext) -> impl IntoElement { - let assistant = cx.view().clone(); - let zoomed = self.zoomed; - PopoverMenu::new("assistant-popover") - .trigger(IconButton::new("trigger", IconName::Menu)) - .menu(move |cx| { - let assistant = assistant.clone(); - ContextMenu::build(cx, |menu, _cx| { - menu.entry( - if zoomed { "Zoom Out" } else { "Zoom In" }, - Some(Box::new(ToggleZoom)), - { - let assistant = assistant.clone(); - move |cx| { - assistant.focus_handle(cx).dispatch_action(&ToggleZoom, cx); - } - }, - ) - .entry("New Context", Some(Box::new(NewFile)), { - let assistant = assistant.clone(); - move |cx| { - assistant.focus_handle(cx).dispatch_action(&NewFile, cx); - } - }) - .entry("History", Some(Box::new(ToggleHistory)), { - let assistant = assistant.clone(); - move |cx| assistant.update(cx, |assistant, cx| assistant.show_history(cx)) - }) - }) - .into() - }) - } - - fn render_inject_context_menu(&self, cx: &mut ViewContext) -> impl Element { - let commands = self.slash_commands.clone(); - let assistant_panel = cx.view().downgrade(); - let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| { - Some( - workspace - .read(cx) - .active_item_as::(cx)? - .focus_handle(cx), - ) - }); - - PopoverMenu::new("inject-context-menu") - .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| { - Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx) - })) - .menu(move |cx| { - ContextMenu::build(cx, |mut menu, _cx| { - for command_name in commands.featured_command_names() { - if let Some(command) = commands.command(&command_name) { - let menu_text = SharedString::from(Arc::from(command.menu_text())); - menu = menu.custom_entry( - { - let command_name = command_name.clone(); - move |_cx| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(menu_text.clone())) - .child( - div().ml_4().child( - Label::new(format!("/{command_name}")) - .color(Color::Muted), - ), - ) - .into_any() - } - }, - { - let assistant_panel = assistant_panel.clone(); - move |cx| { - assistant_panel - .update(cx, |assistant_panel, cx| { - assistant_panel.insert_command(&command_name, cx) - }) - .ok(); - } - }, - ) - } - } - - if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() { - menu = menu - .context(active_editor_focus_handle) - .action("Quote Selection", Box::new(QuoteSelection)); - } - - menu - }) - .into() - }) - } - - fn render_send_button(&self, cx: &mut ViewContext) -> Option { - self.active_context_editor.as_ref().map(|context| { - let focus_handle = context.editor.focus_handle(cx); - ButtonLike::new("send_button") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .children( - KeyBinding::for_action_in(&Assist, &focus_handle, cx) - .map(|binding| binding.into_any_element()), - ) - .child(Label::new("Send")) - .on_click(cx.listener(|this, _event, cx| { - if let Some(active_editor) = this.active_context_editor() { - active_editor.update(cx, |editor, cx| editor.assist(&Assist, cx)); - } - })) - }) + Some(self.active_context_editor(cx)?.read(cx).context.clone()) } fn open_context(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { - cx.focus(&self.focus_handle); + let existing_context = self.pane.read(cx).items().find_map(|item| { + item.downcast::() + .filter(|editor| editor.read(cx).context.read(cx).path.as_ref() == Some(&path)) + }); + if let Some(existing_context) = existing_context { + self.show_context(existing_context, cx); + return Task::ready(Ok(())); + } let saved_context = self.context_store.read(cx).load(path.clone(), cx); let fs = self.fs.clone(); @@ -796,7 +638,7 @@ impl AssistantPanel { let saved_context = saved_context.await?; let context = Context::deserialize( saved_context, - path.clone(), + path, languages, slash_commands, Some(telemetry), @@ -827,152 +669,31 @@ impl AssistantPanel { } fn render_signed_in(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let header = TabBar::new("assistant_header") - .start_child(h_flex().gap_1().child(self.render_popover_button(cx))) - .children(self.active_context_editor().map(|editor| { - h_flex() - .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS)) - .flex_1() - .px_2() - .child( - div() - .id("title") - .cursor_pointer() - .on_click(cx.listener(|this, _, cx| this.hide_history(cx))) - .child(Label::new(editor.read(cx).title(cx))), - ) - })) - .end_child( - h_flex() - .gap_2() - .when_some(self.active_context_editor(), |this, editor| { - let context = editor.read(cx).context.clone(); - this.child( - h_flex() - .gap_1() - .child(ModelSelector::new( - self.model_menu_handle.clone(), - self.fs.clone(), - )) - .children(self.render_remaining_tokens(&context, cx)), - ) - .child( - ui::Divider::vertical() - .inset() - .color(ui::DividerColor::Border), - ) - }) - .child( - h_flex() - .gap_1() - .child(self.render_inject_context_menu(cx)) - .child( - IconButton::new("show-prompt-library", IconName::Library) - .icon_size(IconSize::Small) - .on_click({ - let language_registry = self.languages.clone(); - cx.listener(move |_this, _event, cx| { - open_prompt_library(language_registry.clone(), cx) - .detach_and_log_err(cx); - }) - }) - .tooltip(|cx| Tooltip::text("Prompt Library…", cx)), - ), - ), - ); - - let contents = if self.active_context_editor().is_some() { - let mut registrar = DivRegistrar::new( - |panel, cx| panel.toolbar.read(cx).item_of_type::(), - cx, - ); - BufferSearchBar::register(&mut registrar); - registrar.into_div() - } else { - div() - }; + let mut registrar = DivRegistrar::new( + |panel, cx| { + panel + .pane + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + }, + cx, + ); + BufferSearchBar::register(&mut registrar); + let registrar = registrar.into_div(); v_flex() .key_context("AssistantPanel") - .size_full() - .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { - this.new_context(cx); - })) - .on_action(cx.listener(AssistantPanel::toggle_zoom)) - .on_action(cx.listener(AssistantPanel::toggle_history)) - .on_action(cx.listener(AssistantPanel::deploy)) - .on_action(cx.listener(AssistantPanel::select_next_match)) - .on_action(cx.listener(AssistantPanel::select_prev_match)) - .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) - .on_action(cx.listener(AssistantPanel::reset_credentials)) - .on_action(cx.listener(AssistantPanel::toggle_model_selector)) - .track_focus(&self.focus_handle) - .child(header) - .children(if self.toolbar.read(cx).hidden() { - None - } else { - Some(self.toolbar.clone()) - }) - .child(contents.flex_1().child( - if self.show_saved_contexts || self.active_context_editor().is_none() { - div() - .size_full() - .child(self.saved_context_picker.clone()) - .into_any_element() - } else if let Some(editor) = self.active_context_editor() { - let editor = editor.clone(); - div() - .size_full() - .child(editor.clone()) - .child( - h_flex() - .w_full() - .absolute() - .bottom_0() - .p_4() - .justify_end() - .children(self.render_send_button(cx)), - ) - .into_any_element() - } else { - div().into_any_element() - }, - )) - } - - fn render_remaining_tokens( - &self, - context: &Model, - cx: &mut ViewContext, - ) -> Option { - let model = CompletionProvider::global(cx).model(); - let token_count = context.read(cx).token_count()?; - let max_token_count = model.max_token_count(); - - let remaining_tokens = max_token_count as isize - token_count as isize; - let token_count_color = if remaining_tokens <= 0 { - Color::Error - } else if token_count as f32 / max_token_count as f32 >= 0.8 { - Color::Warning - } else { - Color::Muted - }; - - Some( - h_flex() - .gap_0p5() - .child( - Label::new(humanize_token_count(token_count)) - .size(LabelSize::Small) - .color(token_count_color), - ) - .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) - .child( - Label::new(humanize_token_count(max_token_count)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) + .size_full() + .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { + this.new_context(cx); + })) + .on_action(cx.listener(AssistantPanel::deploy_history)) + .on_action(cx.listener(AssistantPanel::deploy_prompt_library)) + .on_action(cx.listener(AssistantPanel::reset_credentials)) + .on_action(cx.listener(AssistantPanel::toggle_model_selector)) + .child(registrar.size_full().child(self.pane.clone())) } } @@ -1032,13 +753,12 @@ impl Panel for AssistantPanel { cx.notify(); } - fn is_zoomed(&self, _: &WindowContext) -> bool { - self.zoomed + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.zoomed = zoomed; - cx.notify(); + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { @@ -1047,7 +767,7 @@ impl Panel for AssistantPanel { cx.spawn(|this, mut cx| async move { load_credentials.await?; this.update(&mut cx, |this, cx| { - if this.is_authenticated(cx) && this.active_context_editor().is_none() { + if this.is_authenticated(cx) && this.active_context_editor(cx).is_none() { this.new_context(cx); } }) @@ -1078,8 +798,8 @@ impl EventEmitter for AssistantPanel {} impl EventEmitter for AssistantPanel {} impl FocusableView for AssistantPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.pane.focus_handle(cx) } } @@ -2275,7 +1995,7 @@ struct PendingCompletion { _task: Task<()>, } -enum ContextEditorEvent { +pub enum ContextEditorEvent { Edited, TabContentChanged, } @@ -2301,6 +2021,8 @@ pub struct ContextEditor { } impl ContextEditor { + const MAX_TAB_TITLE_LEN: usize = 16; + fn new( language_registry: Arc, slash_command_registry: Arc, @@ -2358,6 +2080,7 @@ impl ContextEditor { cx.observe(&context, |_, _, cx| cx.notify()), cx.subscribe(&context, Self::handle_context_event), cx.subscribe(&editor, Self::handle_editor_event), + cx.subscribe(&editor, Self::handle_editor_search_event), ]; let sections = context.read(cx).slash_command_output_sections.clone(); @@ -2874,6 +2597,15 @@ impl ContextEditor { } } + fn handle_editor_search_event( + &mut self, + _: View, + event: &SearchEvent, + cx: &mut ViewContext, + ) { + cx.emit(event.clone()); + } + fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); @@ -2986,7 +2718,7 @@ impl ContextEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(context_editor_view) = panel.read(cx).active_context_editor().cloned() else { + let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else { return; }; let Some(active_editor_view) = workspace @@ -3067,8 +2799,7 @@ impl ContextEditor { // being updated. cx.defer(move |panel, cx| { if let Some(context) = panel - .active_context_editor() - .cloned() + .active_context_editor(cx) .or_else(|| panel.new_context(cx)) { context.update(cx, |context, cx| { @@ -3308,9 +3039,25 @@ impl ContextEditor { .map(|summary| summary.text.clone()) .unwrap_or_else(|| "New Context".into()) } + + fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let focus_handle = self.focus_handle(cx).clone(); + ButtonLike::new("send_button") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .children( + KeyBinding::for_action_in(&Assist, &focus_handle, cx) + .map(|binding| binding.into_any_element()), + ) + .child(Label::new("Send")) + .on_click(move |_event, cx| { + focus_handle.dispatch_action(&Assist, cx); + }) + } } impl EventEmitter for ContextEditor {} +impl EventEmitter for ContextEditor {} impl Render for ContextEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { @@ -3330,7 +3077,16 @@ impl Render for ContextEditor { div() .flex_grow() .bg(cx.theme().colors().editor_background) - .child(self.editor.clone()), + .child(self.editor.clone()) + .child( + h_flex() + .w_full() + .absolute() + .bottom_0() + .p_4() + .justify_end() + .child(self.render_send_button(cx)), + ), ) } } @@ -3341,6 +3097,403 @@ impl FocusableView for ContextEditor { } } +impl Item for ContextEditor { + type Event = ContextEditorEvent; + + fn tab_content( + &self, + params: workspace::item::TabContentParams, + cx: &WindowContext, + ) -> AnyElement { + let color = if params.selected { + Color::Default + } else { + Color::Muted + }; + Label::new(util::truncate_and_trailoff( + &self.title(cx), + Self::MAX_TAB_TITLE_LEN, + )) + .color(color) + .into_any_element() + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + match event { + ContextEditorEvent::Edited => { + f(workspace::item::ItemEvent::Edit); + f(workspace::item::ItemEvent::UpdateBreadcrumbs); + } + ContextEditorEvent::TabContentChanged => { + f(workspace::item::ItemEvent::UpdateTab); + } + } + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { + Some(self.title(cx).into()) + } + + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } + + fn breadcrumbs( + &self, + theme: &theme::Theme, + cx: &AppContext, + ) -> Option> { + let editor = self.editor.read(cx); + let cursor = editor.selections.newest_anchor().head(); + let multibuffer = &editor.buffer().read(cx); + let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?; + + let settings = ThemeSettings::get_global(cx); + + let mut breadcrumbs = Vec::new(); + + let title = self.title(cx); + if title.chars().count() > Self::MAX_TAB_TITLE_LEN { + breadcrumbs.push(BreadcrumbText { + text: title, + highlights: None, + font: Some(settings.buffer_font.clone()), + }); + } + + breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { + text: symbol.text, + highlights: Some(symbol.highlight_ranges), + font: Some(settings.buffer_font.clone()), + })); + Some(breadcrumbs) + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft + } + + fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + Item::set_nav_history(editor, nav_history, cx) + }) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| Item::navigate(editor, data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } +} + +impl SearchableItem for ContextEditor { + type Match = ::Match; + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.clear_matches(cx); + }); + } + + fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.update_matches(matches, cx)); + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.editor + .update(cx, |editor, cx| editor.query_suggestion(cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: &[Self::Match], + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.activate_match(index, matches, cx); + }); + } + + fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.select_matches(matches, cx)); + } + + fn replace( + &mut self, + identifier: &Self::Match, + query: &project::search::SearchQuery, + cx: &mut ViewContext, + ) { + self.editor + .update(cx, |editor, cx| editor.replace(identifier, query, cx)); + } + + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task> { + self.editor + .update(cx, |editor, cx| editor.find_matches(query, cx)) + } + + fn active_match_index( + &mut self, + matches: &[Self::Match], + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |editor, cx| editor.active_match_index(matches, cx)) + } +} + +pub struct ContextEditorToolbarItem { + fs: Arc, + workspace: WeakView, + active_context_editor: Option>, + model_selector_menu_handle: PopoverMenuHandle, +} + +impl ContextEditorToolbarItem { + pub fn new( + workspace: &Workspace, + model_selector_menu_handle: PopoverMenuHandle, + ) -> Self { + Self { + fs: workspace.app_state().fs.clone(), + workspace: workspace.weak_handle(), + active_context_editor: None, + model_selector_menu_handle, + } + } + + fn render_inject_context_menu(&self, cx: &mut ViewContext) -> impl Element { + let commands = SlashCommandRegistry::global(cx); + let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| { + Some( + workspace + .read(cx) + .active_item_as::(cx)? + .focus_handle(cx), + ) + }); + let active_context_editor = self.active_context_editor.clone(); + + PopoverMenu::new("inject-context-menu") + .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| { + Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx) + })) + .menu(move |cx| { + let active_context_editor = active_context_editor.clone()?; + ContextMenu::build(cx, |mut menu, _cx| { + for command_name in commands.featured_command_names() { + if let Some(command) = commands.command(&command_name) { + let menu_text = SharedString::from(Arc::from(command.menu_text())); + menu = menu.custom_entry( + { + let command_name = command_name.clone(); + move |_cx| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(menu_text.clone())) + .child( + div().ml_4().child( + Label::new(format!("/{command_name}")) + .color(Color::Muted), + ), + ) + .into_any() + } + }, + { + let active_context_editor = active_context_editor.clone(); + move |cx| { + active_context_editor + .update(cx, |context_editor, cx| { + context_editor.insert_command(&command_name, cx) + }) + .ok(); + } + }, + ) + } + } + + if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() { + menu = menu + .context(active_editor_focus_handle) + .action("Quote Selection", Box::new(QuoteSelection)); + } + + menu + }) + .into() + }) + } + + fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { + let model = CompletionProvider::global(cx).model(); + let context = &self + .active_context_editor + .as_ref()? + .upgrade()? + .read(cx) + .context; + let token_count = context.read(cx).token_count()?; + let max_token_count = model.max_token_count(); + + let remaining_tokens = max_token_count as isize - token_count as isize; + let token_count_color = if remaining_tokens <= 0 { + Color::Error + } else if token_count as f32 / max_token_count as f32 >= 0.8 { + Color::Warning + } else { + Color::Muted + }; + + Some( + h_flex() + .gap_0p5() + .child( + Label::new(humanize_token_count(token_count)) + .size(LabelSize::Small) + .color(token_count_color), + ) + .child(Label::new("/").size(LabelSize::Small).color(Color::Muted)) + .child( + Label::new(humanize_token_count(max_token_count)) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + } +} + +impl Render for ContextEditorToolbarItem { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + h_flex() + .gap_2() + .child(ModelSelector::new( + self.model_selector_menu_handle.clone(), + self.fs.clone(), + )) + .children(self.render_remaining_tokens(cx)) + .child(self.render_inject_context_menu(cx)) + } +} + +impl ToolbarItemView for ContextEditorToolbarItem { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + self.active_context_editor = active_pane_item + .and_then(|item| item.act_as::(cx)) + .map(|editor| editor.downgrade()); + cx.notify(); + if self.active_context_editor.is_none() { + ToolbarItemLocation::Hidden + } else { + ToolbarItemLocation::PrimaryRight + } + } + + fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext) { + cx.notify(); + } +} + +impl EventEmitter for ContextEditorToolbarItem {} + +pub struct ContextHistory { + picker: View>, + _subscriptions: Vec, + assistant_panel: WeakView, +} + +impl ContextHistory { + fn new( + context_store: Model, + assistant_panel: WeakView, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.new_view(|cx| { + Picker::uniform_list(SavedContextPickerDelegate::new(context_store.clone()), cx) + .modal(false) + .max_height(None) + }); + + let _subscriptions = vec![ + cx.observe(&context_store, |this, _, cx| { + this.picker.update(cx, |picker, cx| picker.refresh(cx)); + }), + cx.subscribe(&picker, Self::handle_picker_event), + ]; + + Self { + picker, + _subscriptions, + assistant_panel, + } + } + + fn handle_picker_event( + &mut self, + _: View>, + event: &SavedContextPickerEvent, + cx: &mut ViewContext, + ) { + let SavedContextPickerEvent::Confirmed { path } = event; + self.assistant_panel + .update(cx, |assistant_panel, cx| { + assistant_panel + .open_context(path.clone(), cx) + .detach_and_log_err(cx); + }) + .ok(); + } +} + +impl Render for ContextHistory { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div().size_full().child(self.picker.clone()) + } +} + +impl FocusableView for ContextHistory { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter<()> for ContextHistory {} + +impl Item for ContextHistory { + type Event = (); + + fn tab_content( + &self, + params: workspace::item::TabContentParams, + _: &WindowContext, + ) -> AnyElement { + let color = if params.selected { + Color::Default + } else { + Color::Muted + }; + Label::new("History").color(color).into_any_element() + } +} + #[derive(Clone, Debug)] struct MessageAnchor { id: MessageId, diff --git a/crates/assistant/src/model_selector.rs b/crates/assistant/src/model_selector.rs index 9b8c6ad5c3908f..636808f8edf4c2 100644 --- a/crates/assistant/src/model_selector.rs +++ b/crates/assistant/src/model_selector.rs @@ -79,6 +79,6 @@ impl RenderOnce for ModelSelector { Tooltip::for_action("Change Model", &ToggleModelSelector, cx) }), ) - .anchor(gpui::AnchorCorner::BottomRight) + .attach(gpui::AnchorCorner::BottomLeft) } } From d450a1d9e6b19380f3978090478fcf6b03e07b51 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, 4 Jul 2024 18:49:17 +0800 Subject: [PATCH 25/40] windows: Fix `package-version-server` (#13821) Now, it can run on windows. ![Screenshot 2024-07-04 173832](https://github.com/zed-industries/zed/assets/14981363/d3c17fe3-6e79-46cd-b9a3-f6655109463c) Release Notes: - N/A --- crates/languages/src/json.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 11e67c15ce9c0b..9890936dba2e2f 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -317,8 +317,11 @@ impl LspAdapter for NodeVersionAdapter { delegate: &dyn LspAdapterDelegate, ) -> Result { let version = latest_version.downcast::().unwrap(); - let destination_path = - container_dir.join(format!("package-version-server-{}", version.name)); + let destination_path = container_dir.join(format!( + "package-version-server-{}{}", + version.name, + std::env::consts::EXE_SUFFIX + )); let destination_container_path = container_dir.join(format!("package-version-server-{}-tmp", version.name)); if fs::metadata(&destination_path).await.is_err() { @@ -340,7 +343,10 @@ impl LspAdapter for NodeVersionAdapter { } fs::copy( - destination_container_path.join("package-version-server"), + destination_container_path.join(format!( + "package-version-server{}", + std::env::consts::EXE_SUFFIX + )), &destination_path, ) .await?; From b1f8fc88a10d6b47583019cc1f7f57858c34e38b Mon Sep 17 00:00:00 2001 From: Keenan Wresch Date: Thu, 4 Jul 2024 10:22:42 -0400 Subject: [PATCH 26/40] Allow Shift + Scroll to Horizontally Scroll in X11 and Wayland (#13676) Release Notes: - Allows shift + scroll horizontal scrolling on X11 and Wayland. [Screencast from 2024-06-29 17-17-59.webm](https://github.com/zed-industries/zed/assets/14155062/2cac77b9-ecc8-4ddb-b08d-b5d964c8dc84) --- crates/gpui/src/platform/linux/wayland/client.rs | 15 +++++++++++++++ crates/gpui/src/platform/linux/x11/client.rs | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 2728a141eef5a2..dd645e959bf8f2 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1508,6 +1508,11 @@ impl Dispatch for WaylandClientStatePtr { if state.axis_source == AxisSource::Wheel { return; } + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; let axis_modifier = match axis { wl_pointer::Axis::VerticalScroll => state.vertical_modifier, wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, @@ -1533,6 +1538,11 @@ impl Dispatch for WaylandClientStatePtr { discrete, } => { state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; let axis_modifier = match axis { wl_pointer::Axis::VerticalScroll => state.vertical_modifier, wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, @@ -1555,6 +1565,11 @@ impl Dispatch for WaylandClientStatePtr { value120, } => { state.scroll_event_received = true; + let axis = if state.modifiers.shift { + wl_pointer::Axis::HorizontalScroll + } else { + axis + }; let axis_modifier = match axis { wl_pointer::Axis::VerticalScroll => state.vertical_modifier, wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier, diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 39c0b0fd6dcd14..27ee1c8ea462b8 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -817,10 +817,15 @@ impl X11Client { if let Some(old_scroll) = old_scroll { let delta_scroll = old_scroll - new_scroll; + let (x, y) = if !modifiers.shift { + (0.0, delta_scroll) + } else { + (delta_scroll, 0.0) + }; window.handle_input(PlatformInput::ScrollWheel( crate::ScrollWheelEvent { position, - delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)), + delta: ScrollDelta::Lines(Point::new(x, y)), modifiers, touch_phase: TouchPhase::default(), }, From e3cd1dd2d06a92eae5e2d16efbc4c22b9f58710a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Jul 2024 10:40:21 -0400 Subject: [PATCH 27/40] docs: Update language pages to indicate whether they are native or from an extension (#13827) This PR updates the language pages in the docs to indicate whether the support is available natively or provided by an extension. Release Notes: - N/A --- docs/src/languages/elixir.md | 3 +-- docs/src/languages/go.md | 2 ++ docs/src/languages/javascript.md | 2 ++ docs/src/languages/python.md | 2 ++ docs/src/languages/ruby.md | 4 +--- docs/src/languages/rust.md | 2 ++ 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/languages/elixir.md b/docs/src/languages/elixir.md index 62f1c5604fbf99..38d81375b8a07d 100644 --- a/docs/src/languages/elixir.md +++ b/docs/src/languages/elixir.md @@ -1,7 +1,6 @@ # Elixir -- Tree Sitter: [tree-sitter-elixir](https://github.com/elixir-lang/tree-sitter-elixir) -- Language Server: [elixir-ls](https://github.com/elixir-lsp/elixir-ls) +Elixir support is available through the [Elixir extension](https://github.com/zed-industries/zed/tree/main/extensions/elixir). ## Choosing a language server diff --git a/docs/src/languages/go.md b/docs/src/languages/go.md index 9dbb3228979710..9c49b8469bb138 100644 --- a/docs/src/languages/go.md +++ b/docs/src/languages/go.md @@ -1,5 +1,7 @@ # Go +Go support is available natively in Zed. + - Tree Sitter: [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) - Language Server: [gopls](https://github.com/golang/tools/tree/master/gopls) diff --git a/docs/src/languages/javascript.md b/docs/src/languages/javascript.md index 7452e200b1f169..25464712a189e6 100644 --- a/docs/src/languages/javascript.md +++ b/docs/src/languages/javascript.md @@ -1,5 +1,7 @@ # JavaScript +JavaScript support is available natively in Zed. + - Tree Sitter: [tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) - Language Server: [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) diff --git a/docs/src/languages/python.md b/docs/src/languages/python.md index c3825cec03582a..0af9c5b35f54c0 100644 --- a/docs/src/languages/python.md +++ b/docs/src/languages/python.md @@ -1,5 +1,7 @@ # Python +Python support is available natively in Zed. + - Tree Sitter: [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python) - Language Server: [pyright](https://github.com/microsoft/pyright) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index f4744cf68b76db..34d3e73198d16d 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -1,7 +1,6 @@ # Ruby -- Tree Sitter: [tree-sitter-ruby](https://github.com/tree-sitter/tree-sitter-ruby) -- Language Servers: [solargraph](https://github.com/castwide/solargraph), [ruby-lsp](https://github.com/Shopify/ruby-lsp) +Ruby support is available through the [Ruby extension](https://github.com/zed-industries/zed/tree/main/extensions/ruby). ## Choosing a language server @@ -82,7 +81,6 @@ Ruby LSP uses pull-based diagnostics which Zed doesn't support yet. We can tell } ``` - ## Using the Tailwind CSS Language Server with Ruby It's possible to use the [Tailwind CSS Language Server](https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme) in Ruby and ERB files. diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 159df95e76ca4f..12ef427c7bbf09 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -1,5 +1,7 @@ # Rust +Rust support is available natively in Zed. + - Tree Sitter: [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) - Language Server: [rust-analyzer](https://github.com/rust-lang/rust-analyzer) From 3a5d116ffebea61e174a185a3b914f6af615d14c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Jul 2024 11:08:38 -0400 Subject: [PATCH 28/40] docs: Update language docs and include links in the sidebar (#13828) This PR updates the supported language docs and adds them to the sidebar for better discoverability. Release Notes: - N/A --- docs/src/SUMMARY.md | 30 ++++++++++++++++++++++++++++++ docs/src/languages/astro.md | 3 +-- docs/src/languages/c.md | 2 ++ docs/src/languages/clojure.md | 3 +-- docs/src/languages/cpp.md | 2 ++ docs/src/languages/csharp.md | 3 +-- docs/src/languages/css.md | 2 ++ docs/src/languages/deno.md | 3 +-- docs/src/languages/elm.md | 6 ++---- docs/src/languages/erb.md | 4 ---- docs/src/languages/erlang.md | 3 +-- docs/src/languages/gleam.md | 3 +-- docs/src/languages/haskell.md | 3 +-- docs/src/languages/html.md | 15 +++++++++++++-- docs/src/languages/json.md | 2 ++ docs/src/languages/lua.md | 3 +-- docs/src/languages/markdown.md | 2 ++ docs/src/languages/ocaml.md | 5 ++--- docs/src/languages/php.md | 3 +-- docs/src/languages/prisma.md | 3 +-- docs/src/languages/proto.md | 2 ++ docs/src/languages/purescript.md | 3 +-- docs/src/languages/racket.md | 3 +-- docs/src/languages/ruby.md | 2 ++ docs/src/languages/scheme.md | 3 +-- docs/src/languages/svelte.md | 3 +-- docs/src/languages/terraform.md | 3 +-- docs/src/languages/toml.md | 3 +-- docs/src/languages/tsx.md | 4 ---- docs/src/languages/typescript.md | 2 ++ docs/src/languages/uiua.md | 3 +-- docs/src/languages/vue.md | 5 ++--- docs/src/languages/yaml.md | 2 ++ docs/src/languages/zig.md | 3 +-- 34 files changed, 85 insertions(+), 56 deletions(-) delete mode 100644 docs/src/languages/erb.md delete mode 100644 docs/src/languages/tsx.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 233be5115326b7..8278991baf754a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -24,12 +24,42 @@ # Language Support +- [Astro](./languages/astro.md) +- [C](./languages/c.md) +- [C++](./languages/cpp.md) +- [C#](./languages/csharp.md) +- [Clojure](./languages/clojure.md) +- [CSS](./languages/css.md) +- [Deno](./languages/deno.md) - [Elixir](./languages/elixir.md) +- [Elm](./languages/elm.md) +- [Erlang](./languages/erlang.md) +- [Gleam](./languages/gleam.md) - [Go](./languages/go.md) +- [Haskell](./languages/haskell.md) +- [HTML](./languages/html.md) - [JavaScript](./languages/javascript.md) +- [JSON](./languages/json.md) +- [Lua](./languages/lua.md) +- [Markdown](./languages/markdown.md) +- [OCaml](./languages/ocaml.md) +- [PHP](./languages/php.md) +- [Prisma](./languages/prisma.md) +- [Proto](./languages/proto.md) +- [PureScript](./languages/purescript.md) - [Python](./languages/python.md) +- [Racket](./languages/racket.md) - [Ruby](./languages/ruby.md) - [Rust](./languages/rust.md) +- [Scheme](./languages/scheme.md) +- [Svelte](./languages/svelte.md) +- [Terraform](./languages/terraform.md) +- [TOML](./languages/toml.md) +- [TypeScript](./languages/typescript.md) +- [Uiua](./languages/uiua.md) +- [Vue](./languages/vue.md) +- [YAML](./languages/yaml.md) +- [Zig](./languages/zig.md) # Developing Zed diff --git a/docs/src/languages/astro.md b/docs/src/languages/astro.md index 6ce15792b368ab..1dbba8178b8d68 100644 --- a/docs/src/languages/astro.md +++ b/docs/src/languages/astro.md @@ -1,4 +1,3 @@ # Astro -- Tree Sitter: [tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro) -- Language Server: [astro](https://github.com/withastro/language-tools/tree/main/packages/language-server) +Astro support is available through the [Astro extension](https://github.com/zed-industries/zed/tree/main/extensions/astro). diff --git a/docs/src/languages/c.md b/docs/src/languages/c.md index 9afddad731e8f3..e9df6197b0c349 100644 --- a/docs/src/languages/c.md +++ b/docs/src/languages/c.md @@ -1,4 +1,6 @@ # C +C support is available natively in Zed. + - Tree Sitter: [tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c) - Language Server: [clangd](https://github.com/clangd/clangd) diff --git a/docs/src/languages/clojure.md b/docs/src/languages/clojure.md index b89e122a563c98..2dd816b1742145 100644 --- a/docs/src/languages/clojure.md +++ b/docs/src/languages/clojure.md @@ -1,4 +1,3 @@ # Clojure -- Tree Sitter: [tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure) -- Language Server: [clojure-lsp](https://github.com/clojure-lsp/clojure-lsp) +Clojure support is available through the [Clojure extension](https://github.com/zed-industries/zed/tree/main/extensions/clojure). diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index cfa183f71cf123..1cc9f99912d776 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -1,4 +1,6 @@ # C++ +C++ support is available natively in Zed. + - Tree Sitter: [tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp) - Language Server: [clangd](https://github.com/clangd/clangd) diff --git a/docs/src/languages/csharp.md b/docs/src/languages/csharp.md index 1de49c503c9656..4d7342c260dde2 100644 --- a/docs/src/languages/csharp.md +++ b/docs/src/languages/csharp.md @@ -1,4 +1,3 @@ # C# -- Tree Sitter: [tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) -- Language Server: [OmniSharp](https://github.com/OmniSharp/omnisharp-roslyn) +C# support is available through the [C# extension](https://github.com/zed-industries/zed/tree/main/extensions/csharp). diff --git a/docs/src/languages/css.md b/docs/src/languages/css.md index 4ac6b3de6ce189..56c55eae058ea5 100644 --- a/docs/src/languages/css.md +++ b/docs/src/languages/css.md @@ -1,4 +1,6 @@ # CSS +CSS support is available natively in Zed. + - Tree Sitter: [tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css) - Language Server: N/A diff --git a/docs/src/languages/deno.md b/docs/src/languages/deno.md index c8c492233e229d..0de0619a716d59 100644 --- a/docs/src/languages/deno.md +++ b/docs/src/languages/deno.md @@ -1,4 +1,3 @@ # Deno -- Tree Sitter: [tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) -- Language Server: [deno](https://github.com/denoland/deno) +Deno support is available through the [Deno extension](https://github.com/zed-industries/zed/tree/main/extensions/deno). diff --git a/docs/src/languages/elm.md b/docs/src/languages/elm.md index 02bd4a88a64818..229768833204bd 100644 --- a/docs/src/languages/elm.md +++ b/docs/src/languages/elm.md @@ -1,7 +1,6 @@ # Elm -- Tree Sitter: [tree-sitter-elm](https://github.com/elm-tooling/tree-sitter-elm) -- Language Server: [elm-language-server](https://github.com/elm-tooling/elm-language-server) +Elm support is available through the [Elm extension](https://github.com/zed-industries/zed/tree/main/extensions/elm). ### Setting up `elm-language-server` @@ -21,5 +20,4 @@ Elm language server can be configured in your `settings.json`, e.g.: } ``` -`elm-format`, `elm-review` and `elm` need to be installed and made available in the environment -or configured in the settings. See the [full list of server settings here](https://github.com/elm-tooling/elm-language-server?tab=readme-ov-file#server-settings). +`elm-format`, `elm-review` and `elm` need to be installed and made available in the environment or configured in the settings. See the [full list of server settings here](https://github.com/elm-tooling/elm-language-server?tab=readme-ov-file#server-settings). diff --git a/docs/src/languages/erb.md b/docs/src/languages/erb.md deleted file mode 100644 index a94b98643614fb..00000000000000 --- a/docs/src/languages/erb.md +++ /dev/null @@ -1,4 +0,0 @@ -# ERB - -- Tree Sitter: [tree-sitter-embedded-template](https://github.com/tree-sitter/tree-sitter-embedded-template) -- Language Server: [solargraph](https://github.com/castwide/solargraph) diff --git a/docs/src/languages/erlang.md b/docs/src/languages/erlang.md index 3343168fafd71f..13997db446d3d0 100644 --- a/docs/src/languages/erlang.md +++ b/docs/src/languages/erlang.md @@ -1,4 +1,3 @@ # Erlang -- Tree Sitter: [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang) -- Language Server: [erlang_ls](https://github.com/erlang-ls/erlang_ls) +Erlang support is available through the [Erlang extension](https://github.com/zed-industries/zed/tree/main/extensions/erlang). diff --git a/docs/src/languages/gleam.md b/docs/src/languages/gleam.md index 78fecda2595aa7..c0459a4d5baeb8 100644 --- a/docs/src/languages/gleam.md +++ b/docs/src/languages/gleam.md @@ -1,4 +1,3 @@ # Gleam -- Tree Sitter: [tree-sitter-gleam](https://github.com/gleam-lang/tree-sitter-gleam) -- Language Server: [gleam](https://github.com/gleam-lang/gleam) +Gleam support is available through the [Gleam extension](https://github.com/zed-industries/zed/tree/main/extensions/gleam). diff --git a/docs/src/languages/haskell.md b/docs/src/languages/haskell.md index c3ac1e88600772..fc273660aaa437 100644 --- a/docs/src/languages/haskell.md +++ b/docs/src/languages/haskell.md @@ -1,4 +1,3 @@ # Haskell -- Tree Sitter: [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell) -- Language Server: [hls](https://github.com/haskell/haskell-language-server) +Haskell support is available through the [Haskell extension](https://github.com/zed-industries/zed/tree/main/extensions/haskell). diff --git a/docs/src/languages/html.md b/docs/src/languages/html.md index f435959436b159..a45936c8f98c61 100644 --- a/docs/src/languages/html.md +++ b/docs/src/languages/html.md @@ -1,4 +1,15 @@ # HTML -- Tree Sitter: [tree-sitter-html](https://github.com/tree-sitter/tree-sitter-html) -- Language Server: [vscode-html-language-server](https://github.com/hrsh7th/vscode-langservers-extracted) +HTML support is available through the [HTML extension](https://github.com/zed-industries/zed/tree/main/extensions/html). + +This extension is automatically installed. + +If you do not want to use the HTML extension, you can add the following to your settings: + +```json +{ + "auto_install_extensions": { + "html": false + } +} +``` diff --git a/docs/src/languages/json.md b/docs/src/languages/json.md index 2eaa90b3215c00..d77f5f34091c06 100644 --- a/docs/src/languages/json.md +++ b/docs/src/languages/json.md @@ -1,4 +1,6 @@ # JSON +JSON support is available natively in Zed. + - Tree Sitter: [tree-sitter-json](https://github.com/tree-sitter/tree-sitter-json) - Language Server: [json-language-server](https://github.com/zed-industries/json-language-server) diff --git a/docs/src/languages/lua.md b/docs/src/languages/lua.md index b2fa23bd273ccc..ae869ebf8d122b 100644 --- a/docs/src/languages/lua.md +++ b/docs/src/languages/lua.md @@ -1,4 +1,3 @@ # Lua -- Tree Sitter: [tree-sitter-lua](https://github.com/MunifTanjim/tree-sitter-lua) -- Language Server: [lua-language-server](https://github.com/LuaLS/lua-language-server) +Lua support is available through the [Lua extension](https://github.com/zed-industries/zed/tree/main/extensions/lua). diff --git a/docs/src/languages/markdown.md b/docs/src/languages/markdown.md index ec3dadc8fad4e4..1351e47e805f84 100644 --- a/docs/src/languages/markdown.md +++ b/docs/src/languages/markdown.md @@ -1,4 +1,6 @@ # Markdown +Markdown support is available natively in Zed. + - Tree Sitter: [tree-sitter-markdown](https://github.com/MDeiml/tree-sitter-markdown) - Language Server: N/A diff --git a/docs/src/languages/ocaml.md b/docs/src/languages/ocaml.md index 6670b942e1780e..fdcf79d74824df 100644 --- a/docs/src/languages/ocaml.md +++ b/docs/src/languages/ocaml.md @@ -1,13 +1,12 @@ # OCaml -- Tree Sitter: [tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) -- Language Server: [ocamllsp](https://github.com/ocaml/ocaml-lsp) +OCaml support is available through the [OCaml extension](https://github.com/zed-industries/zed/tree/main/extensions/ocaml). ## Setup Instructions If you have the development environment already setup, you can skip to [Launching Zed](#launching-zed) -### Using OPAM +### Using Opam Opam is the official package manager for OCaml and is highly recommended for getting started with OCaml. To get started using Opam, please follow the instructions provided [here](https://ocaml.org/install). diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md index 37393b604fd299..880efa67c48bac 100644 --- a/docs/src/languages/php.md +++ b/docs/src/languages/php.md @@ -1,4 +1,3 @@ # PHP -- Tree Sitter: [tree-sitter-php](https://github.com/tree-sitter/tree-sitter-php) -- Language Server: [intelephense](https://intelephense.com/) +PHP support is available through the [PHP extension](https://github.com/zed-industries/zed/tree/main/extensions/php). diff --git a/docs/src/languages/prisma.md b/docs/src/languages/prisma.md index 935a8d3efe162c..9302fca17fce67 100644 --- a/docs/src/languages/prisma.md +++ b/docs/src/languages/prisma.md @@ -1,4 +1,3 @@ # Prisma -- Tree Sitter: [tree-sitter-prisma](https://github.com/victorhqc/tree-sitter-prisma) -- Language Server: [prisma-language-server](https://github.com/prisma/language-tools/tree/main/packages/language-server) +Prisma support is available through the [Prisma extension](https://github.com/zed-industries/zed/tree/main/extensions/prisma). diff --git a/docs/src/languages/proto.md b/docs/src/languages/proto.md index 486542a76cda52..73aaff68bce003 100644 --- a/docs/src/languages/proto.md +++ b/docs/src/languages/proto.md @@ -1,4 +1,6 @@ # Proto +Proto support is available natively in Zed. + - Tree-Sitter: [tree-sitter-proto](https://github.com/rewinfrey/tree-sitter-proto) - Language-Server: N/A diff --git a/docs/src/languages/purescript.md b/docs/src/languages/purescript.md index 1c46886e58120b..f34ef23176dce8 100644 --- a/docs/src/languages/purescript.md +++ b/docs/src/languages/purescript.md @@ -1,4 +1,3 @@ # PureScript -- Tree Sitter: [tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript) -- Language Server: [purescript](https://github.com/nwolverson/purescript-language-server) +PureScript support is available through the [PureScript extension](https://github.com/zed-industries/zed/tree/main/extensions/purescript). diff --git a/docs/src/languages/racket.md b/docs/src/languages/racket.md index 73e6de0f9a6595..379a5c83480fc1 100644 --- a/docs/src/languages/racket.md +++ b/docs/src/languages/racket.md @@ -1,4 +1,3 @@ # Racket -- Tree Sitter: [tree-sitter-racket](https://github.com/zed-industries/tree-sitter-racket) -- Language Server: N/A +Racket support is available through the [Racket extension](https://github.com/zed-industries/zed/tree/main/extensions/racket). diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 34d3e73198d16d..ab8f549ba49c05 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -2,6 +2,8 @@ Ruby support is available through the [Ruby extension](https://github.com/zed-industries/zed/tree/main/extensions/ruby). +The Ruby extension also provides support for ERB files. + ## Choosing a language server The Ruby extension offers both `solargraph` and `ruby-lsp` language server support. diff --git a/docs/src/languages/scheme.md b/docs/src/languages/scheme.md index 4662e8e183d328..3be418b037205c 100644 --- a/docs/src/languages/scheme.md +++ b/docs/src/languages/scheme.md @@ -1,4 +1,3 @@ # Scheme -- Tree Sitter: [tree-sitter-scheme](https://github.com/6cdh/tree-sitter-scheme) -- Language Server: N/A +Scheme support is available through the [Scheme extension](https://github.com/zed-industries/zed/tree/main/extensions/scheme). diff --git a/docs/src/languages/svelte.md b/docs/src/languages/svelte.md index e6b128189364bf..793b3174924f88 100644 --- a/docs/src/languages/svelte.md +++ b/docs/src/languages/svelte.md @@ -1,7 +1,6 @@ # Svelte -- Tree Sitter: [tree-sitter-svelte](https://github.com/Himujjal/tree-sitter-svelte) -- Language Server: [svelte](https://github.com/sveltejs/language-tools/tree/master/packages/language-server) +Svelte support is available through the [Svelte extension](https://github.com/zed-industries/zed/tree/main/extensions/svelte). ## Inlay Hints diff --git a/docs/src/languages/terraform.md b/docs/src/languages/terraform.md index 32bfe9b4b1dd49..894db30dea1868 100644 --- a/docs/src/languages/terraform.md +++ b/docs/src/languages/terraform.md @@ -1,7 +1,6 @@ # Terraform -- Tree Sitter: [tree-sitter-hcl](https://github.com/MichaHoffmann/tree-sitter-hcl) -- Language Server: [terraform-ls](https://github.com/hashicorp/terraform-ls) +Terraform support is available through the [Terraform extension](https://github.com/zed-industries/zed/tree/main/extensions/terraform). ### Configuration diff --git a/docs/src/languages/toml.md b/docs/src/languages/toml.md index 7b923ca507c64e..5a415dfe897ddf 100644 --- a/docs/src/languages/toml.md +++ b/docs/src/languages/toml.md @@ -1,4 +1,3 @@ # TOML -- Tree Sitter: [tree-sitter-toml](https://github.com/tree-sitter/tree-sitter-toml) -- Language Server: [taplo](https://taplo.tamasfe.dev) +TOML support is available through the [TOML extension](https://github.com/zed-industries/zed/tree/main/extensions/toml). diff --git a/docs/src/languages/tsx.md b/docs/src/languages/tsx.md deleted file mode 100644 index 63b4e927fc2d78..00000000000000 --- a/docs/src/languages/tsx.md +++ /dev/null @@ -1,4 +0,0 @@ -# TSX - -- Tree Sitter: [tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) -- Language Server: [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) diff --git a/docs/src/languages/typescript.md b/docs/src/languages/typescript.md index 7210e169e9307e..ce9d988dd9fceb 100644 --- a/docs/src/languages/typescript.md +++ b/docs/src/languages/typescript.md @@ -1,5 +1,7 @@ # TypeScript +TypeScript and TSX support are available natively in Zed. + - Tree Sitter: [tree-sitter-typescript](https://github.com/tree-sitter/tree-sitter-typescript) - Language Server: [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) diff --git a/docs/src/languages/uiua.md b/docs/src/languages/uiua.md index d969424f5af18f..4e2a039c6d61da 100644 --- a/docs/src/languages/uiua.md +++ b/docs/src/languages/uiua.md @@ -1,4 +1,3 @@ # Uiua -- Tree Sitter: [tree-sitter-uiua](https://github.com/shnarazk/tree-sitter-uiua) -- Language Server: [uiua](https://github.com/uiua-lang/uiua) +Uiua support is available through the [Uiua extension](https://github.com/zed-industries/zed/tree/main/extensions/uiua). diff --git a/docs/src/languages/vue.md b/docs/src/languages/vue.md index 83c5bccbc07d4a..38d06a6edcacb2 100644 --- a/docs/src/languages/vue.md +++ b/docs/src/languages/vue.md @@ -1,6 +1,5 @@ # Vue -- Tree Sitter: [tree-sitter-vue](https://github.com/zed-industries/tree-sitter-vue) -- Language Server: [@vue/language-server](https://github.com/vuejs/language-tools/tree/master/packages/language-server) +Vue support is available through the [Vue extension](https://github.com/zed-industries/zed/tree/main/extensions/vue). -> Pinned `@vue/language-server` to version 1.8 until Zed supports 2.x #9846 +> `@vue/language-server` is pinned to v1.8 due to some issues in v2.x [#9846](https://github.com/zed-industries/zed/pull/9846) diff --git a/docs/src/languages/yaml.md b/docs/src/languages/yaml.md index 65e0f21145f999..a0ad86bf22732c 100644 --- a/docs/src/languages/yaml.md +++ b/docs/src/languages/yaml.md @@ -1,4 +1,6 @@ # YAML +YAML support is available natively in Zed. + - Tree Sitter: [tree-sitter-yaml](https://github.com/zed-industries/tree-sitter-yaml) - Language Server: [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) diff --git a/docs/src/languages/zig.md b/docs/src/languages/zig.md index ab94b4c9568377..3eca03192d64d7 100644 --- a/docs/src/languages/zig.md +++ b/docs/src/languages/zig.md @@ -1,4 +1,3 @@ # Zig -- Tree Sitter: [tree-sitter-zig](https://github.com/maxxnino/tree-sitter-zig) -- Language Server: [zls](https://github.com/zigtools/zls) +Zig support is available through the [Zig extension](https://github.com/zed-industries/zed/tree/main/extensions/zig). From 398c2f91dd2aa56a6270ecc742d4fd396fe765f1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 4 Jul 2024 11:14:39 -0400 Subject: [PATCH 29/40] docs: Add Dart (#13829) This PR adds Dart support to the docs, as it was one of the first-party extensions that wasn't mentioned anywhere. Release Notes: - N/A --- docs/src/SUMMARY.md | 1 + docs/src/languages/dart.md | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 docs/src/languages/dart.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 8278991baf754a..c43e78abbb25e8 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -30,6 +30,7 @@ - [C#](./languages/csharp.md) - [Clojure](./languages/clojure.md) - [CSS](./languages/css.md) +- [Dart](./languages/dart.md) - [Deno](./languages/deno.md) - [Elixir](./languages/elixir.md) - [Elm](./languages/elm.md) diff --git a/docs/src/languages/dart.md b/docs/src/languages/dart.md new file mode 100644 index 00000000000000..5c95cb078151a0 --- /dev/null +++ b/docs/src/languages/dart.md @@ -0,0 +1,3 @@ +# Dart + +Dart support is available through the [Dart extension](https://github.com/zed-industries/zed/tree/main/extensions/dart). From c8b106245c7e62353e0cd595213b166e03378130 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 4 Jul 2024 18:29:08 +0200 Subject: [PATCH 30/40] linux/x11: Resize on GTK_EDGE_CONSTRAINTS atom (#13833) With the new window decorations resizing was _really_ laggy on my X11 machine. Before: - Click on window border (hitbox doesn't work properly, but that's another issue) - Drag and resize - 4-5s nothing happens - Window is resized After: - Click on window border - Drag and resize - Window is resized I'm still not 100% sure on why this happens on my machine and not Conrad's/Mikayla's, but seems like that GTK_EDGE_CONSTRAINTS atom is sent when resizing. The other thing that I can't explain is that we get a `ConfigureNotify` when resizing, with the right size, but maybe not often enough? Anyway, for now we'll go with this. Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/window.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 52fccaf272c087..6d4dbcf2a170ed 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -54,6 +54,7 @@ x11rb::atom_manager! { _MOTIF_WM_HINTS, _GTK_SHOW_WINDOW_MENU, _GTK_FRAME_EXTENTS, + _GTK_EDGE_CONSTRAINTS, } } @@ -681,7 +682,9 @@ impl X11WindowStatePtr { pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) { let mut state = self.state.borrow_mut(); - if event.atom == state.atoms._NET_WM_STATE { + if event.atom == state.atoms._NET_WM_STATE + || event.atom == state.atoms._GTK_EDGE_CONSTRAINTS + { self.set_wm_properties(state); } } From 56e3fc794a363896dea532346a2b9a6244b050c8 Mon Sep 17 00:00:00 2001 From: Owen Law <81528246+someone13574@users.noreply.github.com> Date: Fri, 5 Jul 2024 03:16:28 -0400 Subject: [PATCH 31/40] linux/x11: Fix bugs related to unflushed commands (#13844) **Edit**: This PR adds flushes to functions which should have an immediate affect. I've observed it fixing the following bugs (but there are probably more): - The cursor not updating when just hovering. - The window not maximising after clicking the full-screen button until you move the mouse. - The window not minimising after clicking the minimise button until you move the mouse. --- **Original content**: Following #13646, the cursor style wouldn't change because the `change_window_attributes` command wasn't being flushed. I guess it was working before because something else was flushing it somewhere else so it was never noticed. I just added `check()` which flushes the command so that the cursor will actually update when just hovering. Before you would need to interact with the window so that something else could flush the command. Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/client.rs | 4 +++- crates/gpui/src/platform/linux/x11/window.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 27ee1c8ea462b8..11d2efb43b4ec5 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1072,7 +1072,9 @@ impl LinuxClient for X11Client { ..Default::default() }, ) - .expect("failed to change window cursor"); + .expect("failed to change window cursor") + .check() + .unwrap(); } fn open_uri(&self, uri: &str) { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 6d4dbcf2a170ed..5ca04d509f604a 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -607,6 +607,8 @@ impl X11Window { EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, ) + .unwrap() + .check() .unwrap(); } @@ -994,6 +996,7 @@ impl PlatformWindow for X11Window { xproto::Time::CURRENT_TIME, ) .log_err(); + self.0.xcb_connection.flush().unwrap(); } fn is_active(&self) -> bool { @@ -1022,6 +1025,7 @@ impl PlatformWindow for X11Window { title.as_bytes(), ) .unwrap(); + self.0.xcb_connection.flush().unwrap(); } fn set_app_id(&mut self, app_id: &str) { @@ -1039,6 +1043,8 @@ impl PlatformWindow for X11Window { xproto::AtomEnum::STRING, &data, ) + .unwrap() + .check() .unwrap(); } @@ -1074,6 +1080,8 @@ impl PlatformWindow for X11Window { EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, ) + .unwrap() + .check() .unwrap(); } @@ -1164,6 +1172,8 @@ impl PlatformWindow for X11Window { EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, ) + .unwrap() + .check() .unwrap(); } @@ -1226,6 +1236,8 @@ impl PlatformWindow for X11Window { 4, bytemuck::cast_slice::(&insets), ) + .unwrap() + .check() .unwrap(); } } @@ -1250,6 +1262,8 @@ impl PlatformWindow for X11Window { 5, bytemuck::cast_slice::(&hints_data), ) + .unwrap() + .check() .unwrap(); match decorations { From 2a923e338f9c3025547d26db07fd867fc73c7cb4 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 5 Jul 2024 10:55:45 +0200 Subject: [PATCH 32/40] linux/x11: Set transparency to false by default (#13848) The previous code lead to a ton of error messages from Blade on my X11 machine, even with *client-side decorations working well!* As @someone13574 pointed out [here](https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030) things still work with this being false. And if someone changes a theme with transparent background, then it will get set anyway. We just don't do it by default. And as @jansol pointed out [here](https://github.com/zed-industries/zed/issues/5040#issuecomment-2096560629): > On X11 it may be possible to configure a compositor to blur window backgrounds but Zed has no way to influence that. So we don't lose anything, I think, but get rid of a ton of error messages in the logs. Proof of shadows etc. still working: ![Screenshot from 2024-07-05 10-17-38](https://github.com/zed-industries/zed/assets/1185253/1216b38b-8011-46e7-b86f-c0f5fc3f6f64) Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/window.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 5ca04d509f604a..3539f9733842f9 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -204,8 +204,7 @@ pub struct X11WindowState { impl X11WindowState { fn is_transparent(&self) -> bool { - self.decorations == WindowDecorations::Client - || self.background_appearance != WindowBackgroundAppearance::Opaque + self.background_appearance != WindowBackgroundAppearance::Opaque } } @@ -441,8 +440,11 @@ impl X11WindowState { // Note: this has to be done after the GPU init, or otherwise // the sizes are immediately invalidated. size: query_render_extent(xcb_connection, x_window), - // In case we have window decorations to render - transparent: true, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, }; xcb_connection.map_window(x_window).unwrap(); From fc8749ffd796cdfd19ea21df1a2d126b4f960fd6 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 5 Jul 2024 11:33:31 +0200 Subject: [PATCH 33/40] linux: Set directory in SaveFileRequest dialog (#13850) This has been bugging me for a while. If you create a new file and then save it, the dialogue would show the home directory and not the folder that you were in. This fixes it. Release Notes: - N/A --- crates/gpui/src/platform/linux/platform.rs | 29 ++++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 9eef459d5dc710..0e46419b3929fe 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -314,20 +314,27 @@ impl Platform for P { let directory = directory.to_owned(); self.foreground_executor() .spawn(async move { - let result = SaveFileRequest::default() + let request = SaveFileRequest::default() .modal(true) .title("Select new path") .accept_label("Accept") - .send() - .await - .ok() - .and_then(|request| request.response().ok()) - .and_then(|response| { - response - .uris() - .first() - .and_then(|uri| uri.to_file_path().ok()) - }); + .current_folder(directory); + + let result = if let Ok(request) = request { + request + .send() + .await + .ok() + .and_then(|request| request.response().ok()) + .and_then(|response| { + response + .uris() + .first() + .and_then(|uri| uri.to_file_path().ok()) + }) + } else { + None + }; done_tx.send(result); }) From 1260b52c82179d3dce82e0014028147d77b5d0e8 Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Fri, 5 Jul 2024 16:49:05 +0530 Subject: [PATCH 34/40] linux scripts: Respect `$CARGO_TARGET_DIR` (#13830) https://doc.rust-lang.org/cargo/reference/environment-variables.html Some people (myself included) set this variable to have a single directory to clean up (or whatever reason one might have for having a single `target` directory). This changes the linux scripts to respect that Release Notes: - N/A --- script/bundle-linux | 15 ++++++++------- script/install-linux | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/script/bundle-linux b/script/bundle-linux index cc4ce9fa1bdfa3..f06d2497b3c955 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -26,6 +26,7 @@ done export ZED_BUNDLE=true channel=$( Date: Fri, 5 Jul 2024 14:27:53 +0200 Subject: [PATCH 35/40] ui: Don't show tooltip when ButtonLike is selected (#13857) This fixes the issue of a tooltip covering the thing that the button has revealed. It also mirrors what other UI frameworks do. Chrome on Linux behaves the same, and Safari does the same thing on macOS. It fixes this: ![Screenshot from 2024-07-05 12-39-22](https://github.com/zed-industries/zed/assets/1185253/51ce4347-d12f-463a-9adb-da031fe2f751) Release Notes: - N/A --- crates/ui/src/components/button/button_like.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index ee865fa364dfb3..3fea9d867c5e0c 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -507,8 +507,10 @@ impl RenderOnce for ButtonLike { }) }, ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |cx| tooltip(cx)) + .when(!self.selected, |this| { + this.when_some(self.tooltip, |this, tooltip| { + this.tooltip(move |cx| tooltip(cx)) + }) }) .children(self.children) } From c4dbe32f203d985c21fa7377e24879e8f417163b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 5 Jul 2024 14:52:45 +0200 Subject: [PATCH 36/40] assistant: Limit amount of concurrent completion requests (#13856) This PR refactors the completion providers to only process a maximum amount of completion requests at a time. Also started refactoring language model providers to use traits, so it's easier to allow specifying multiple providers in the future. Release Notes: - N/A --- crates/assistant/src/assistant.rs | 2 +- crates/assistant/src/assistant_panel.rs | 21 +- crates/assistant/src/assistant_settings.rs | 3 +- crates/assistant/src/completion_provider.rs | 533 ++++++++++-------- .../src/completion_provider/anthropic.rs | 106 ++-- .../src/completion_provider/cloud.rs | 57 +- .../assistant/src/completion_provider/fake.rs | 98 +++- .../src/completion_provider/ollama.rs | 202 +++---- .../src/completion_provider/open_ai.rs | 113 ++-- crates/assistant/src/inline_assistant.rs | 37 +- .../src/terminal_inline_assistant.rs | 5 +- 11 files changed, 669 insertions(+), 508 deletions(-) diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 9d2e8c01426f2b..9b7d4e3f735e4b 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -163,7 +163,7 @@ impl LanguageModelRequestMessage { } } -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct LanguageModelRequest { pub model: LanguageModel, pub messages: Vec, diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index f097338e01fb64..5ba8447087a7ab 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1409,7 +1409,7 @@ impl Context { } let request = self.to_completion_request(cx); - let stream = CompletionProvider::global(cx).complete(request); + let response = CompletionProvider::global(cx).complete(request, cx); let assistant_message = self .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) .unwrap(); @@ -1422,11 +1422,12 @@ impl Context { let task = cx.spawn({ |this, mut cx| async move { + let response = response.await; let assistant_message_id = assistant_message.id; let mut response_latency = None; let stream_completion = async { let request_start = Instant::now(); - let mut messages = stream.await?; + let mut messages = response.inner.await?; while let Some(message) = messages.next().await { if response_latency.is_none() { @@ -1718,10 +1719,11 @@ impl Context { temperature: 1.0, }; - let stream = CompletionProvider::global(cx).complete(request); + let response = CompletionProvider::global(cx).complete(request, cx); self.pending_summary = cx.spawn(|this, mut cx| { async move { - let mut messages = stream.await?; + let response = response.await; + let mut messages = response.inner.await?; while let Some(message) = messages.next().await { let text = message?; @@ -3642,7 +3644,7 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { let settings_store = SettingsStore::test(cx); - cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); + FakeCompletionProvider::setup_test(cx); cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); @@ -3774,7 +3776,7 @@ mod tests { fn test_message_splitting(cx: &mut AppContext) { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); + FakeCompletionProvider::setup_test(cx); init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); @@ -3867,7 +3869,7 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { let settings_store = SettingsStore::test(cx); - cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); + FakeCompletionProvider::setup_test(cx); cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); @@ -3952,7 +3954,8 @@ mod tests { async fn test_slash_commands(cx: &mut TestAppContext) { let settings_store = cx.update(SettingsStore::test); cx.set_global(settings_store); - cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); + cx.update(|cx| FakeCompletionProvider::setup_test(cx)); + cx.update(Project::init_settings); cx.update(init); let fs = FakeFs::new(cx.background_executor.clone()); @@ -4147,7 +4150,7 @@ mod tests { async fn test_serialization(cx: &mut TestAppContext) { let settings_store = cx.update(SettingsStore::test); cx.set_global(settings_store); - cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); + cx.update(FakeCompletionProvider::setup_test); cx.update(init); let registry = Arc::new(LanguageRegistry::test(cx.executor())); let context = diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 2d5218fac2da69..8ff078512a6f19 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -1,5 +1,6 @@ use std::fmt; +use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest}; pub use anthropic::Model as AnthropicModel; use gpui::Pixels; pub use ollama::Model as OllamaModel; @@ -15,8 +16,6 @@ use serde::{ use settings::{Settings, SettingsSources}; use strum::{EnumIter, IntoEnumIterator}; -use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest}; - #[derive(Clone, Debug, Default, PartialEq, EnumIter)] pub enum CloudModel { Gpt3Point5Turbo, diff --git a/crates/assistant/src/completion_provider.rs b/crates/assistant/src/completion_provider.rs index 8badd32d84feb7..36a5bc883e720f 100644 --- a/crates/assistant/src/completion_provider.rs +++ b/crates/assistant/src/completion_provider.rs @@ -11,6 +11,8 @@ pub use cloud::*; pub use fake::*; pub use ollama::*; pub use open_ai::*; +use parking_lot::RwLock; +use smol::lock::{Semaphore, SemaphoreGuardArc}; use crate::{ assistant_settings::{AssistantProvider, AssistantSettings}, @@ -21,8 +23,8 @@ use client::Client; use futures::{future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext}; use settings::{Settings, SettingsStore}; -use std::sync::Arc; use std::time::Duration; +use std::{any::Any, sync::Arc}; /// Choose which model to use for openai provider. /// If the model is not available, try to use the first available model, or fallback to the original model. @@ -39,272 +41,91 @@ fn choose_openai_model( } pub fn init(client: Arc, cx: &mut AppContext) { - let mut settings_version = 0; - let provider = match &AssistantSettings::get_global(cx).provider { - AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud( - CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx), - ), - AssistantProvider::OpenAi { - model, - api_url, - low_speed_timeout_in_seconds, - available_models, - } => CompletionProvider::OpenAi(OpenAiCompletionProvider::new( - choose_openai_model(model, available_models), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - )), - AssistantProvider::Anthropic { - model, - api_url, - low_speed_timeout_in_seconds, - } => CompletionProvider::Anthropic(AnthropicCompletionProvider::new( - model.clone(), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - )), - AssistantProvider::Ollama { - model, - api_url, - low_speed_timeout_in_seconds, - } => CompletionProvider::Ollama(OllamaCompletionProvider::new( - model.clone(), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - cx, - )), - }; - cx.set_global(provider); + let provider = create_provider_from_settings(client.clone(), 0, cx); + cx.set_global(CompletionProvider::new(provider, Some(client))); + let mut settings_version = 0; cx.observe_global::(move |cx| { settings_version += 1; cx.update_global::(|provider, cx| { - match (&mut *provider, &AssistantSettings::get_global(cx).provider) { - ( - CompletionProvider::OpenAi(provider), - AssistantProvider::OpenAi { - model, - api_url, - low_speed_timeout_in_seconds, - available_models, - }, - ) => { - provider.update( - choose_openai_model(model, available_models), - api_url.clone(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - ); - } - ( - CompletionProvider::Anthropic(provider), - AssistantProvider::Anthropic { - model, - api_url, - low_speed_timeout_in_seconds, - }, - ) => { - provider.update( - model.clone(), - api_url.clone(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - ); - } - - ( - CompletionProvider::Ollama(provider), - AssistantProvider::Ollama { - model, - api_url, - low_speed_timeout_in_seconds, - }, - ) => { - provider.update( - model.clone(), - api_url.clone(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - cx, - ); - } - - (CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => { - provider.update(model.clone(), settings_version); - } - (_, AssistantProvider::ZedDotDev { model }) => { - *provider = CompletionProvider::Cloud(CloudCompletionProvider::new( - model.clone(), - client.clone(), - settings_version, - cx, - )); - } - ( - _, - AssistantProvider::OpenAi { - model, - api_url, - low_speed_timeout_in_seconds, - available_models, - }, - ) => { - *provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new( - choose_openai_model(model, available_models), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - )); - } - ( - _, - AssistantProvider::Anthropic { - model, - api_url, - low_speed_timeout_in_seconds, - }, - ) => { - *provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new( - model.clone(), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - )); - } - ( - _, - AssistantProvider::Ollama { - model, - api_url, - low_speed_timeout_in_seconds, - }, - ) => { - *provider = CompletionProvider::Ollama(OllamaCompletionProvider::new( - model.clone(), - api_url.clone(), - client.http_client(), - low_speed_timeout_in_seconds.map(Duration::from_secs), - settings_version, - cx, - )); - } - } + provider.update_settings(settings_version, cx); }) }) .detach(); } -pub enum CompletionProvider { - OpenAi(OpenAiCompletionProvider), - Anthropic(AnthropicCompletionProvider), - Cloud(CloudCompletionProvider), - #[cfg(test)] - Fake(FakeCompletionProvider), - Ollama(OllamaCompletionProvider), +pub struct CompletionResponse { + pub inner: BoxFuture<'static, Result>>>, + _lock: SemaphoreGuardArc, } -impl gpui::Global for CompletionProvider {} +pub trait LanguageModelCompletionProvider: Send + Sync { + fn available_models(&self, cx: &AppContext) -> Vec; + fn settings_version(&self) -> usize; + fn is_authenticated(&self) -> bool; + fn authenticate(&self, cx: &AppContext) -> Task>; + fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView; + fn reset_credentials(&self, cx: &AppContext) -> Task>; + fn model(&self) -> LanguageModel; + fn count_tokens( + &self, + request: LanguageModelRequest, + cx: &AppContext, + ) -> BoxFuture<'static, Result>; + fn complete( + &self, + request: LanguageModelRequest, + ) -> BoxFuture<'static, Result>>>; + + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4; + +pub struct CompletionProvider { + provider: Arc>, + client: Option>, + request_limiter: Arc, +} impl CompletionProvider { - pub fn global(cx: &AppContext) -> &Self { - cx.global::() + pub fn new( + provider: Arc>, + client: Option>, + ) -> Self { + Self { + provider, + client, + request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_COMPLETION_REQUESTS)), + } } pub fn available_models(&self, cx: &AppContext) -> Vec { - match self { - CompletionProvider::OpenAi(provider) => provider - .available_models(cx) - .map(LanguageModel::OpenAi) - .collect(), - CompletionProvider::Anthropic(provider) => provider - .available_models() - .map(LanguageModel::Anthropic) - .collect(), - CompletionProvider::Cloud(provider) => provider - .available_models() - .map(LanguageModel::Cloud) - .collect(), - CompletionProvider::Ollama(provider) => provider - .available_models() - .map(|model| LanguageModel::Ollama(model.clone())) - .collect(), - #[cfg(test)] - CompletionProvider::Fake(_) => unimplemented!(), - } + self.provider.read().available_models(cx) } pub fn settings_version(&self) -> usize { - match self { - CompletionProvider::OpenAi(provider) => provider.settings_version(), - CompletionProvider::Anthropic(provider) => provider.settings_version(), - CompletionProvider::Cloud(provider) => provider.settings_version(), - CompletionProvider::Ollama(provider) => provider.settings_version(), - #[cfg(test)] - CompletionProvider::Fake(_) => unimplemented!(), - } + self.provider.read().settings_version() } pub fn is_authenticated(&self) -> bool { - match self { - CompletionProvider::OpenAi(provider) => provider.is_authenticated(), - CompletionProvider::Anthropic(provider) => provider.is_authenticated(), - CompletionProvider::Cloud(provider) => provider.is_authenticated(), - CompletionProvider::Ollama(provider) => provider.is_authenticated(), - #[cfg(test)] - CompletionProvider::Fake(_) => true, - } + self.provider.read().is_authenticated() } pub fn authenticate(&self, cx: &AppContext) -> Task> { - match self { - CompletionProvider::OpenAi(provider) => provider.authenticate(cx), - CompletionProvider::Anthropic(provider) => provider.authenticate(cx), - CompletionProvider::Cloud(provider) => provider.authenticate(cx), - CompletionProvider::Ollama(provider) => provider.authenticate(cx), - #[cfg(test)] - CompletionProvider::Fake(_) => Task::ready(Ok(())), - } + self.provider.read().authenticate(cx) } pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { - match self { - CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx), - CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx), - CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx), - CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx), - #[cfg(test)] - CompletionProvider::Fake(_) => unimplemented!(), - } + self.provider.read().authentication_prompt(cx) } pub fn reset_credentials(&self, cx: &AppContext) -> Task> { - match self { - CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx), - CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx), - CompletionProvider::Cloud(_) => Task::ready(Ok(())), - CompletionProvider::Ollama(provider) => provider.reset_credentials(cx), - #[cfg(test)] - CompletionProvider::Fake(_) => Task::ready(Ok(())), - } + self.provider.read().reset_credentials(cx) } pub fn model(&self) -> LanguageModel { - match self { - CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()), - CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()), - CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()), - CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()), - #[cfg(test)] - CompletionProvider::Fake(_) => LanguageModel::default(), - } + self.provider.read().model() } pub fn count_tokens( @@ -312,27 +133,241 @@ impl CompletionProvider { request: LanguageModelRequest, cx: &AppContext, ) -> BoxFuture<'static, Result> { - match self { - CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx), - CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx), - CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx), - CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx), - #[cfg(test)] - CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))), - } + self.provider.read().count_tokens(request, cx) } pub fn complete( &self, request: LanguageModelRequest, - ) -> BoxFuture<'static, Result>>> { - match self { - CompletionProvider::OpenAi(provider) => provider.complete(request), - CompletionProvider::Anthropic(provider) => provider.complete(request), - CompletionProvider::Cloud(provider) => provider.complete(request), - CompletionProvider::Ollama(provider) => provider.complete(request), - #[cfg(test)] - CompletionProvider::Fake(provider) => provider.complete(), + cx: &AppContext, + ) -> Task { + let rate_limiter = self.request_limiter.clone(); + let provider = self.provider.clone(); + cx.background_executor().spawn(async move { + let lock = rate_limiter.acquire_arc().await; + let response = provider.read().complete(request); + CompletionResponse { + inner: response, + _lock: lock, + } + }) + } +} + +impl gpui::Global for CompletionProvider {} + +impl CompletionProvider { + pub fn global(cx: &AppContext) -> &Self { + cx.global::() + } + + pub fn update_current_as( + &mut self, + update: impl FnOnce(&mut T) -> R, + ) -> Option { + let mut provider = self.provider.write(); + if let Some(provider) = provider.as_any_mut().downcast_mut::() { + Some(update(provider)) + } else { + None + } + } + + pub fn update_settings(&mut self, version: usize, cx: &mut AppContext) { + let updated = match &AssistantSettings::get_global(cx).provider { + AssistantProvider::ZedDotDev { model } => self + .update_current_as::<_, CloudCompletionProvider>(|provider| { + provider.update(model.clone(), version); + }), + AssistantProvider::OpenAi { + model, + api_url, + low_speed_timeout_in_seconds, + available_models, + } => self.update_current_as::<_, OpenAiCompletionProvider>(|provider| { + provider.update( + choose_openai_model(&model, &available_models), + api_url.clone(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + version, + ); + }), + AssistantProvider::Anthropic { + model, + api_url, + low_speed_timeout_in_seconds, + } => self.update_current_as::<_, AnthropicCompletionProvider>(|provider| { + provider.update( + model.clone(), + api_url.clone(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + version, + ); + }), + AssistantProvider::Ollama { + model, + api_url, + low_speed_timeout_in_seconds, + } => self.update_current_as::<_, OllamaCompletionProvider>(|provider| { + provider.update( + model.clone(), + api_url.clone(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + version, + cx, + ); + }), + }; + + // Previously configured provider was changed to another one + if updated.is_none() { + if let Some(client) = self.client.clone() { + self.provider = create_provider_from_settings(client, version, cx); + } else { + log::warn!("completion provider cannot be created because client is not set"); + } } } } + +fn create_provider_from_settings( + client: Arc, + settings_version: usize, + cx: &mut AppContext, +) -> Arc> { + match &AssistantSettings::get_global(cx).provider { + AssistantProvider::ZedDotDev { model } => Arc::new(RwLock::new( + CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx), + )), + AssistantProvider::OpenAi { + model, + api_url, + low_speed_timeout_in_seconds, + available_models, + } => Arc::new(RwLock::new(OpenAiCompletionProvider::new( + choose_openai_model(&model, &available_models), + api_url.clone(), + client.http_client(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + settings_version, + ))), + AssistantProvider::Anthropic { + model, + api_url, + low_speed_timeout_in_seconds, + } => Arc::new(RwLock::new(AnthropicCompletionProvider::new( + model.clone(), + api_url.clone(), + client.http_client(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + settings_version, + ))), + AssistantProvider::Ollama { + model, + api_url, + low_speed_timeout_in_seconds, + } => Arc::new(RwLock::new(OllamaCompletionProvider::new( + model.clone(), + api_url.clone(), + client.http_client(), + low_speed_timeout_in_seconds.map(Duration::from_secs), + settings_version, + cx, + ))), + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use gpui::AppContext; + use parking_lot::RwLock; + use settings::SettingsStore; + use smol::stream::StreamExt; + + use crate::{ + completion_provider::MAX_CONCURRENT_COMPLETION_REQUESTS, CompletionProvider, + FakeCompletionProvider, LanguageModelRequest, + }; + + #[gpui::test] + fn test_rate_limiting(cx: &mut AppContext) { + SettingsStore::test(cx); + let fake_provider = FakeCompletionProvider::setup_test(cx); + + let provider = CompletionProvider::new(Arc::new(RwLock::new(fake_provider.clone())), None); + + // Enqueue some requests + for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 { + let response = provider.complete( + LanguageModelRequest { + temperature: i as f32 / 10.0, + ..Default::default() + }, + cx, + ); + cx.background_executor() + .spawn(async move { + let response = response.await; + let mut stream = response.inner.await.unwrap(); + while let Some(message) = stream.next().await { + message.unwrap(); + } + }) + .detach(); + } + cx.background_executor().run_until_parked(); + + assert_eq!( + fake_provider.completion_count(), + MAX_CONCURRENT_COMPLETION_REQUESTS + ); + + // Get the first completion request that is in flight and mark it as completed. + let completion = fake_provider + .running_completions() + .into_iter() + .next() + .unwrap(); + fake_provider.finish_completion(&completion); + + // Ensure that the number of in-flight completion requests is reduced. + assert_eq!( + fake_provider.completion_count(), + MAX_CONCURRENT_COMPLETION_REQUESTS - 1 + ); + + cx.background_executor().run_until_parked(); + + // Ensure that another completion request was allowed to acquire the lock. + assert_eq!( + fake_provider.completion_count(), + MAX_CONCURRENT_COMPLETION_REQUESTS + ); + + // Mark all completion requests as finished that are in flight. + for request in fake_provider.running_completions() { + fake_provider.finish_completion(&request); + } + + assert_eq!(fake_provider.completion_count(), 0); + + // Wait until the background tasks acquire the lock again. + cx.background_executor().run_until_parked(); + + assert_eq!( + fake_provider.completion_count(), + MAX_CONCURRENT_COMPLETION_REQUESTS - 1 + ); + + // Finish all remaining completion requests. + for request in fake_provider.running_completions() { + fake_provider.finish_completion(&request); + } + + cx.background_executor().run_until_parked(); + + assert_eq!(fake_provider.completion_count(), 0); + } +} diff --git a/crates/assistant/src/completion_provider/anthropic.rs b/crates/assistant/src/completion_provider/anthropic.rs index 87236501a94d30..b4c573588bcadd 100644 --- a/crates/assistant/src/completion_provider/anthropic.rs +++ b/crates/assistant/src/completion_provider/anthropic.rs @@ -2,7 +2,7 @@ use crate::{ assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role, }; -use crate::{count_open_ai_tokens, LanguageModelRequestMessage}; +use crate::{count_open_ai_tokens, LanguageModelCompletionProvider, LanguageModelRequestMessage}; use anthropic::{stream_completion, Request, RequestMessage}; use anyhow::{anyhow, Result}; use editor::{Editor, EditorElement, EditorStyle}; @@ -26,50 +26,22 @@ pub struct AnthropicCompletionProvider { settings_version: usize, } -impl AnthropicCompletionProvider { - pub fn new( - model: AnthropicModel, - api_url: String, - http_client: Arc, - low_speed_timeout: Option, - settings_version: usize, - ) -> Self { - Self { - api_key: None, - api_url, - model, - http_client, - low_speed_timeout, - settings_version, - } - } - - pub fn update( - &mut self, - model: AnthropicModel, - api_url: String, - low_speed_timeout: Option, - settings_version: usize, - ) { - self.model = model; - self.api_url = api_url; - self.low_speed_timeout = low_speed_timeout; - self.settings_version = settings_version; - } - - pub fn available_models(&self) -> impl Iterator { +impl LanguageModelCompletionProvider for AnthropicCompletionProvider { + fn available_models(&self, _cx: &AppContext) -> Vec { AnthropicModel::iter() + .map(LanguageModel::Anthropic) + .collect() } - pub fn settings_version(&self) -> usize { + fn settings_version(&self) -> usize { self.settings_version } - pub fn is_authenticated(&self) -> bool { + fn is_authenticated(&self) -> bool { self.api_key.is_some() } - pub fn authenticate(&self, cx: &AppContext) -> Task> { + fn authenticate(&self, cx: &AppContext) -> Task> { if self.is_authenticated() { Task::ready(Ok(())) } else { @@ -85,36 +57,36 @@ impl AnthropicCompletionProvider { String::from_utf8(api_key)? }; cx.update_global::(|provider, _cx| { - if let CompletionProvider::Anthropic(provider) = provider { + provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| { provider.api_key = Some(api_key); - } + }); }) }) } } - pub fn reset_credentials(&self, cx: &AppContext) -> Task> { + fn reset_credentials(&self, cx: &AppContext) -> Task> { let delete_credentials = cx.delete_credentials(&self.api_url); cx.spawn(|mut cx| async move { delete_credentials.await.log_err(); cx.update_global::(|provider, _cx| { - if let CompletionProvider::Anthropic(provider) = provider { + provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| { provider.api_key = None; - } + }); }) }) } - pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { + fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx)) .into() } - pub fn model(&self) -> AnthropicModel { - self.model.clone() + fn model(&self) -> LanguageModel { + LanguageModel::Anthropic(self.model.clone()) } - pub fn count_tokens( + fn count_tokens( &self, request: LanguageModelRequest, cx: &AppContext, @@ -122,7 +94,7 @@ impl AnthropicCompletionProvider { count_open_ai_tokens(request, cx.background_executor()) } - pub fn complete( + fn complete( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { @@ -167,12 +139,48 @@ impl AnthropicCompletionProvider { .boxed() } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +impl AnthropicCompletionProvider { + pub fn new( + model: AnthropicModel, + api_url: String, + http_client: Arc, + low_speed_timeout: Option, + settings_version: usize, + ) -> Self { + Self { + api_key: None, + api_url, + model, + http_client, + low_speed_timeout, + settings_version, + } + } + + pub fn update( + &mut self, + model: AnthropicModel, + api_url: String, + low_speed_timeout: Option, + settings_version: usize, + ) { + self.model = model; + self.api_url = api_url; + self.low_speed_timeout = low_speed_timeout; + self.settings_version = settings_version; + } + fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request { preprocess_anthropic_request(&mut request); let model = match request.model { LanguageModel::Anthropic(model) => model, - _ => self.model(), + _ => self.model.clone(), }; let mut system_message = String::new(); @@ -278,9 +286,9 @@ impl AuthenticationPrompt { cx.spawn(|_, mut cx| async move { write_credentials.await?; cx.update_global::(|provider, _cx| { - if let CompletionProvider::Anthropic(provider) = provider { + provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| { provider.api_key = Some(api_key); - } + }); }) }) .detach_and_log_err(cx); diff --git a/crates/assistant/src/completion_provider/cloud.rs b/crates/assistant/src/completion_provider/cloud.rs index 1112def5196aae..c02e531ee980f8 100644 --- a/crates/assistant/src/completion_provider/cloud.rs +++ b/crates/assistant/src/completion_provider/cloud.rs @@ -1,6 +1,6 @@ use crate::{ assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel, - LanguageModelRequest, + LanguageModelCompletionProvider, LanguageModelRequest, }; use anyhow::{anyhow, Result}; use client::{proto, Client}; @@ -30,11 +30,9 @@ impl CloudCompletionProvider { let maintain_client_status = cx.spawn(|mut cx| async move { while let Some(status) = status_rx.next().await { let _ = cx.update_global::(|provider, _cx| { - if let CompletionProvider::Cloud(provider) = provider { + provider.update_current_as::<_, Self>(|provider| { provider.status = status; - } else { - unreachable!() - } + }); }); } }); @@ -51,44 +49,53 @@ impl CloudCompletionProvider { self.model = model; self.settings_version = settings_version; } +} - pub fn available_models(&self) -> impl Iterator { +impl LanguageModelCompletionProvider for CloudCompletionProvider { + fn available_models(&self, _cx: &AppContext) -> Vec { let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() { Some(custom_model) } else { None }; - CloudModel::iter().filter_map(move |model| { - if let CloudModel::Custom(_) = model { - Some(CloudModel::Custom(custom_model.take()?)) - } else { - Some(model) - } - }) + CloudModel::iter() + .filter_map(move |model| { + if let CloudModel::Custom(_) = model { + Some(CloudModel::Custom(custom_model.take()?)) + } else { + Some(model) + } + }) + .map(LanguageModel::Cloud) + .collect() } - pub fn settings_version(&self) -> usize { + fn settings_version(&self) -> usize { self.settings_version } - pub fn model(&self) -> CloudModel { - self.model.clone() - } - - pub fn is_authenticated(&self) -> bool { + fn is_authenticated(&self) -> bool { self.status.is_connected() } - pub fn authenticate(&self, cx: &AppContext) -> Task> { + fn authenticate(&self, cx: &AppContext) -> Task> { let client = self.client.clone(); cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).await }) } - pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { + fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { cx.new_view(|_cx| AuthenticationPrompt).into() } - pub fn count_tokens( + fn reset_credentials(&self, _cx: &AppContext) -> Task> { + Task::ready(Ok(())) + } + + fn model(&self) -> LanguageModel { + LanguageModel::Cloud(self.model.clone()) + } + + fn count_tokens( &self, request: LanguageModelRequest, cx: &AppContext, @@ -128,7 +135,7 @@ impl CloudCompletionProvider { } } - pub fn complete( + fn complete( &self, mut request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { @@ -161,6 +168,10 @@ impl CloudCompletionProvider { }) .boxed() } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } } struct AuthenticationPrompt; diff --git a/crates/assistant/src/completion_provider/fake.rs b/crates/assistant/src/completion_provider/fake.rs index 9c06796a376c74..f07a3befd27d1c 100644 --- a/crates/assistant/src/completion_provider/fake.rs +++ b/crates/assistant/src/completion_provider/fake.rs @@ -1,29 +1,107 @@ use anyhow::Result; +use collections::HashMap; use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; +use gpui::{AnyView, AppContext, Task}; use std::sync::Arc; +use ui::WindowContext; + +use crate::{LanguageModel, LanguageModelCompletionProvider, LanguageModelRequest}; #[derive(Clone, Default)] pub struct FakeCompletionProvider { - current_completion_tx: Arc>>>, + current_completion_txs: Arc>>>, } impl FakeCompletionProvider { - pub fn complete(&self) -> BoxFuture<'static, Result>>> { - let (tx, rx) = mpsc::unbounded(); - *self.current_completion_tx.lock() = Some(tx); - async move { Ok(rx.map(Ok).boxed()) }.boxed() + #[cfg(test)] + pub fn setup_test(cx: &mut AppContext) -> Self { + use crate::CompletionProvider; + use parking_lot::RwLock; + + let this = Self::default(); + let provider = CompletionProvider::new(Arc::new(RwLock::new(this.clone())), None); + cx.set_global(provider); + this + } + + pub fn running_completions(&self) -> Vec { + self.current_completion_txs + .lock() + .keys() + .map(|k| serde_json::from_str(k).unwrap()) + .collect() + } + + pub fn completion_count(&self) -> usize { + self.current_completion_txs.lock().len() } - pub fn send_completion(&self, chunk: String) { - self.current_completion_tx + pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) { + let json = serde_json::to_string(request).unwrap(); + self.current_completion_txs .lock() - .as_ref() + .get(&json) .unwrap() .unbounded_send(chunk) .unwrap(); } - pub fn finish_completion(&self) { - self.current_completion_tx.lock().take(); + pub fn finish_completion(&self, request: &LanguageModelRequest) { + self.current_completion_txs + .lock() + .remove(&serde_json::to_string(request).unwrap()); + } +} + +impl LanguageModelCompletionProvider for FakeCompletionProvider { + fn available_models(&self, _cx: &AppContext) -> Vec { + vec![LanguageModel::default()] + } + + fn settings_version(&self) -> usize { + 0 + } + + fn is_authenticated(&self) -> bool { + true + } + + fn authenticate(&self, _cx: &AppContext) -> Task> { + Task::ready(Ok(())) + } + + fn authentication_prompt(&self, _cx: &mut WindowContext) -> AnyView { + unimplemented!() + } + + fn reset_credentials(&self, _cx: &AppContext) -> Task> { + Task::ready(Ok(())) + } + + fn model(&self) -> LanguageModel { + LanguageModel::default() + } + + fn count_tokens( + &self, + _request: LanguageModelRequest, + _cx: &AppContext, + ) -> BoxFuture<'static, Result> { + futures::future::ready(Ok(0)).boxed() + } + + fn complete( + &self, + _request: LanguageModelRequest, + ) -> BoxFuture<'static, Result>>> { + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs + .lock() + .insert(serde_json::to_string(&_request).unwrap(), tx); + async move { Ok(rx.map(Ok).boxed()) }.boxed() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self } } diff --git a/crates/assistant/src/completion_provider/ollama.rs b/crates/assistant/src/completion_provider/ollama.rs index e3a80de5325244..f782a2035556b2 100644 --- a/crates/assistant/src/completion_provider/ollama.rs +++ b/crates/assistant/src/completion_provider/ollama.rs @@ -1,3 +1,4 @@ +use crate::LanguageModelCompletionProvider; use crate::{ assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role, }; @@ -26,6 +27,108 @@ pub struct OllamaCompletionProvider { available_models: Vec, } +impl LanguageModelCompletionProvider for OllamaCompletionProvider { + fn available_models(&self, _cx: &AppContext) -> Vec { + self.available_models + .iter() + .map(|m| LanguageModel::Ollama(m.clone())) + .collect() + } + + fn settings_version(&self) -> usize { + self.settings_version + } + + fn is_authenticated(&self) -> bool { + !self.available_models.is_empty() + } + + fn authenticate(&self, cx: &AppContext) -> Task> { + if self.is_authenticated() { + Task::ready(Ok(())) + } else { + self.fetch_models(cx) + } + } + + fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { + let fetch_models = Box::new(move |cx: &mut WindowContext| { + cx.update_global::(|provider, cx| { + provider + .update_current_as::<_, OllamaCompletionProvider>(|provider| { + provider.fetch_models(cx) + }) + .unwrap_or_else(|| Task::ready(Ok(()))) + }) + }); + + cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx)) + .into() + } + + fn reset_credentials(&self, cx: &AppContext) -> Task> { + self.fetch_models(cx) + } + + fn model(&self) -> LanguageModel { + LanguageModel::Ollama(self.model.clone()) + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + _cx: &AppContext, + ) -> BoxFuture<'static, Result> { + // There is no endpoint for this _yet_ in Ollama + // see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582 + let token_count = request + .messages + .iter() + .map(|msg| msg.content.chars().count()) + .sum::() + / 4; + + async move { Ok(token_count) }.boxed() + } + + fn complete( + &self, + request: LanguageModelRequest, + ) -> BoxFuture<'static, Result>>> { + let request = self.to_ollama_request(request); + + let http_client = self.http_client.clone(); + let api_url = self.api_url.clone(); + let low_speed_timeout = self.low_speed_timeout; + async move { + let request = + stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout); + let response = request.await?; + let stream = response + .filter_map(|response| async move { + match response { + Ok(delta) => { + let content = match delta.message { + ChatMessage::User { content } => content, + ChatMessage::Assistant { content } => content, + ChatMessage::System { content } => content, + }; + Some(Ok(content)) + } + Err(error) => Some(Err(error)), + } + }) + .boxed(); + Ok(stream) + } + .boxed() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + impl OllamaCompletionProvider { pub fn new( model: OllamaModel, @@ -87,36 +190,12 @@ impl OllamaCompletionProvider { self.settings_version = settings_version; } - pub fn available_models(&self) -> impl Iterator { - self.available_models.iter() - } - pub fn select_first_available_model(&mut self) { if let Some(model) = self.available_models.first() { self.model = model.clone(); } } - pub fn settings_version(&self) -> usize { - self.settings_version - } - - pub fn is_authenticated(&self) -> bool { - !self.available_models.is_empty() - } - - pub fn authenticate(&self, cx: &AppContext) -> Task> { - if self.is_authenticated() { - Task::ready(Ok(())) - } else { - self.fetch_models(cx) - } - } - - pub fn reset_credentials(&self, cx: &AppContext) -> Task> { - self.fetch_models(cx) - } - pub fn fetch_models(&self, cx: &AppContext) -> Task> { let http_client = self.http_client.clone(); let api_url = self.api_url.clone(); @@ -137,90 +216,21 @@ impl OllamaCompletionProvider { models.sort_by(|a, b| a.name.cmp(&b.name)); cx.update_global::(|provider, _cx| { - if let CompletionProvider::Ollama(provider) = provider { + provider.update_current_as::<_, OllamaCompletionProvider>(|provider| { provider.available_models = models; if !provider.available_models.is_empty() && provider.model.name.is_empty() { provider.select_first_available_model() } - } + }); }) }) } - pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { - let fetch_models = Box::new(move |cx: &mut WindowContext| { - cx.update_global::(|provider, cx| { - if let CompletionProvider::Ollama(provider) = provider { - provider.fetch_models(cx) - } else { - Task::ready(Ok(())) - } - }) - }); - - cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx)) - .into() - } - - pub fn model(&self) -> OllamaModel { - self.model.clone() - } - - pub fn count_tokens( - &self, - request: LanguageModelRequest, - _cx: &AppContext, - ) -> BoxFuture<'static, Result> { - // There is no endpoint for this _yet_ in Ollama - // see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582 - let token_count = request - .messages - .iter() - .map(|msg| msg.content.chars().count()) - .sum::() - / 4; - - async move { Ok(token_count) }.boxed() - } - - pub fn complete( - &self, - request: LanguageModelRequest, - ) -> BoxFuture<'static, Result>>> { - let request = self.to_ollama_request(request); - - let http_client = self.http_client.clone(); - let api_url = self.api_url.clone(); - let low_speed_timeout = self.low_speed_timeout; - async move { - let request = - stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout); - let response = request.await?; - let stream = response - .filter_map(|response| async move { - match response { - Ok(delta) => { - let content = match delta.message { - ChatMessage::User { content } => content, - ChatMessage::Assistant { content } => content, - ChatMessage::System { content } => content, - }; - Some(Ok(content)) - } - Err(error) => Some(Err(error)), - } - }) - .boxed(); - Ok(stream) - } - .boxed() - } - fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest { let model = match request.model { LanguageModel::Ollama(model) => model, - _ => self.model(), + _ => self.model.clone(), }; ChatRequest { diff --git a/crates/assistant/src/completion_provider/open_ai.rs b/crates/assistant/src/completion_provider/open_ai.rs index f4459faf149691..6c16e2c9a609a1 100644 --- a/crates/assistant/src/completion_provider/open_ai.rs +++ b/crates/assistant/src/completion_provider/open_ai.rs @@ -1,5 +1,6 @@ use crate::assistant_settings::CloudModel; use crate::assistant_settings::{AssistantProvider, AssistantSettings}; +use crate::LanguageModelCompletionProvider; use crate::{ assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role, }; @@ -57,37 +58,75 @@ impl OpenAiCompletionProvider { self.settings_version = settings_version; } - pub fn available_models(&self, cx: &AppContext) -> impl Iterator { + fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request { + let model = match request.model { + LanguageModel::OpenAi(model) => model, + _ => self.model.clone(), + }; + + Request { + model, + messages: request + .messages + .into_iter() + .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, + } + } +} + +impl LanguageModelCompletionProvider for OpenAiCompletionProvider { + fn available_models(&self, cx: &AppContext) -> Vec { if let AssistantProvider::OpenAi { available_models, .. } = &AssistantSettings::get_global(cx).provider { if !available_models.is_empty() { - // available_models is set, just return it - return available_models.clone().into_iter(); + return available_models + .iter() + .cloned() + .map(LanguageModel::OpenAi) + .collect(); } } let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) { - // available_models is not set but the default model is set to custom, only show custom vec![self.model.clone()] } else { - // default case, use all models except custom OpenAiModel::iter() .filter(|model| !matches!(model, OpenAiModel::Custom { .. })) .collect() }; - available_models.into_iter() + available_models + .into_iter() + .map(LanguageModel::OpenAi) + .collect() } - pub fn settings_version(&self) -> usize { + fn settings_version(&self) -> usize { self.settings_version } - pub fn is_authenticated(&self) -> bool { + fn is_authenticated(&self) -> bool { self.api_key.is_some() } - pub fn authenticate(&self, cx: &AppContext) -> Task> { + fn authenticate(&self, cx: &AppContext) -> Task> { if self.is_authenticated() { Task::ready(Ok(())) } else { @@ -103,36 +142,36 @@ impl OpenAiCompletionProvider { String::from_utf8(api_key)? }; cx.update_global::(|provider, _cx| { - if let CompletionProvider::OpenAi(provider) = provider { + provider.update_current_as::<_, Self>(|provider| { provider.api_key = Some(api_key); - } + }); }) }) } } - pub fn reset_credentials(&self, cx: &AppContext) -> Task> { + fn reset_credentials(&self, cx: &AppContext) -> Task> { let delete_credentials = cx.delete_credentials(&self.api_url); cx.spawn(|mut cx| async move { delete_credentials.await.log_err(); cx.update_global::(|provider, _cx| { - if let CompletionProvider::OpenAi(provider) = provider { + provider.update_current_as::<_, Self>(|provider| { provider.api_key = None; - } + }); }) }) } - pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { + fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView { cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx)) .into() } - pub fn model(&self) -> OpenAiModel { - self.model.clone() + fn model(&self) -> LanguageModel { + LanguageModel::OpenAi(self.model.clone()) } - pub fn count_tokens( + fn count_tokens( &self, request: LanguageModelRequest, cx: &AppContext, @@ -140,7 +179,7 @@ impl OpenAiCompletionProvider { count_open_ai_tokens(request, cx.background_executor()) } - pub fn complete( + fn complete( &self, request: LanguageModelRequest, ) -> BoxFuture<'static, Result>>> { @@ -173,36 +212,8 @@ impl OpenAiCompletionProvider { .boxed() } - fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request { - let model = match request.model { - LanguageModel::OpenAi(model) => model, - _ => self.model(), - }; - - Request { - model, - messages: request - .messages - .into_iter() - .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, - } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self } } @@ -284,9 +295,9 @@ impl AuthenticationPrompt { cx.spawn(|_, mut cx| async move { write_credentials.await?; cx.update_global::(|provider, _cx| { - if let CompletionProvider::OpenAi(provider) = provider { + provider.update_current_as::<_, OpenAiCompletionProvider>(|provider| { provider.api_key = Some(api_key); - } + }); }) }) .detach_and_log_err(cx); diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 3c3a799126475e..4ea5696ca72736 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1986,13 +1986,14 @@ impl Codegen { .unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row))); let model_telemetry_id = prompt.model.telemetry_id(); - let response = CompletionProvider::global(cx).complete(prompt); + let response = CompletionProvider::global(cx).complete(prompt, cx); let telemetry = self.telemetry.clone(); self.edit_position = range.start; self.diff = Diff::default(); self.status = CodegenStatus::Pending; self.generation = cx.spawn(|this, mut cx| { async move { + let response = response.await; let generate = async { let mut edit_start = range.start.to_offset(&snapshot); @@ -2002,7 +2003,7 @@ impl Codegen { let mut response_latency = None; let request_start = Instant::now(); let diff = async { - let chunks = StripInvalidSpans::new(response.await?); + let chunks = StripInvalidSpans::new(response.inner.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -2473,9 +2474,8 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { - let provider = FakeCompletionProvider::default(); cx.set_global(cx.update(SettingsStore::test)); - cx.set_global(CompletionProvider::Fake(provider.clone())); + let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.update(language_settings::init); let text = indoc! {" @@ -2495,8 +2495,11 @@ mod tests { }); let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx)); - let request = LanguageModelRequest::default(); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + codegen.update(cx, |codegen, cx| { + codegen.start(LanguageModelRequest::default(), cx) + }); + + cx.background_executor.run_until_parked(); let mut new_text = concat!( " let mut x = 0;\n", @@ -2508,11 +2511,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(chunk.into()); + provider.send_completion(&LanguageModelRequest::default(), chunk.into()); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(); + provider.finish_completion(&LanguageModelRequest::default()); cx.background_executor.run_until_parked(); assert_eq!( @@ -2533,8 +2536,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - let provider = FakeCompletionProvider::default(); - cx.set_global(CompletionProvider::Fake(provider.clone())); + let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); @@ -2555,6 +2557,8 @@ mod tests { let request = LanguageModelRequest::default(); codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + cx.background_executor.run_until_parked(); + let mut new_text = concat!( "t mut x = 0;\n", "while x < 10 {\n", @@ -2565,11 +2569,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(chunk.into()); + provider.send_completion(&LanguageModelRequest::default(), chunk.into()); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(); + provider.finish_completion(&LanguageModelRequest::default()); cx.background_executor.run_until_parked(); assert_eq!( @@ -2590,8 +2594,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - let provider = FakeCompletionProvider::default(); - cx.set_global(CompletionProvider::Fake(provider.clone())); + let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx)); cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); @@ -2612,6 +2615,8 @@ mod tests { let request = LanguageModelRequest::default(); codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + cx.background_executor.run_until_parked(); + let mut new_text = concat!( "let mut x = 0;\n", "while x < 10 {\n", @@ -2622,11 +2627,11 @@ mod tests { let max_len = cmp::min(new_text.len(), 10); let len = rng.gen_range(1..=max_len); let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(chunk.into()); + provider.send_completion(&LanguageModelRequest::default(), chunk.into()); new_text = suffix; cx.background_executor.run_until_parked(); } - provider.finish_completion(); + provider.finish_completion(&LanguageModelRequest::default()); cx.background_executor.run_until_parked(); assert_eq!( diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 13c52a29bb03cb..ac0bb9af910e7c 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1026,9 +1026,10 @@ impl Codegen { let telemetry = self.telemetry.clone(); let model_telemetry_id = prompt.model.telemetry_id(); - let response = CompletionProvider::global(cx).complete(prompt); + let response = CompletionProvider::global(cx).complete(prompt, cx); self.generation = cx.spawn(|this, mut cx| async move { + let response = response.await; let generate = async { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); @@ -1036,7 +1037,7 @@ impl Codegen { let mut response_latency = None; let request_start = Instant::now(); let task = async { - let mut response = response.await?; + let mut response = response.inner.await?; while let Some(chunk) = response.next().await { if response_latency.is_none() { response_latency = Some(request_start.elapsed()); From e69f9d6cf9da4cac8d8101215db94808b5f01a54 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 5 Jul 2024 15:02:14 +0200 Subject: [PATCH 37/40] linux/x11: Fix gap when tiling windows side by side (#13859) By leveraging the `_GTK_EDGE_CONSTRAINTS` atom we can get all four booleans for the `Tiling` struct and figure out which side is free when the window is tiled to half of the screen. For the logic behind the `_GTK_EDGE_CONSTRAINTS` see: - https://github.com/GNOME/mutter/blob/8e9d13aa3b3baa099ca32bbcb10afb4d9eea4460/src/x11/window-x11.c#L65-L75 - https://github.com/GNOME/mutter/blob/8e9d13aa3b3baa099ca32bbcb10afb4d9eea4460/src/x11/window-x11.c#L1205-L1231 (I used Claude 3.5 Sonnet with our code and these pieces from `mutter` to generate the Rust code, that was pretty sweet) This fixes the gap in the middle when a GPUI window is tiled to the left and another window to the right. It's not _perfect_ but it looks a lot better. Here's a diff that makes it look better: ```diff diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index 122231f6b..7fa29dadc 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -72,8 +72,8 @@ impl Render for WindowShadow { .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding)) .when(!tiling.top, |div| div.pt(shadow_size)) .when(!tiling.bottom, |div| div.pb(shadow_size)) - .when(!tiling.left, |div| div.pl(shadow_size)) - .when(!tiling.right, |div| div.pr(shadow_size)) + .when(!tiling.left, |div| div.pl(shadow_size - border_size)) + .when(!tiling.right, |div| div.pr(shadow_size - border_size)) .on_mouse_move(|_e, cx| cx.refresh()) .on_mouse_down(MouseButton::Left, move |e, cx| { let size = cx.window_bounds().get_bounds().size; ``` But that makes it look weird on Wayland, so I didn't do it. I think it's fine for now. Chromium looks bad and has a gap, so we're already better. ## Before ![before_1](https://github.com/zed-industries/zed/assets/1185253/875c5cdd-c0be-4295-beb0-bb9ba5beaa52) ![before_2](https://github.com/zed-industries/zed/assets/1185253/0b96be70-4c34-4e99-aeb2-ab741171ad14) ## After ![after_1](https://github.com/zed-industries/zed/assets/1185253/aa51da77-daf1-4ef8-a33f-a83731e0c7e1) ![after_2](https://github.com/zed-industries/zed/assets/1185253/8ce7902d-90b6-4f06-ba2c-626e643abe56) Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/window.rs | 114 ++++++++++++++++--- crates/workspace/src/workspace.rs | 2 + 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 3539f9733842f9..d81de2a48e4c48 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -86,6 +86,49 @@ impl ResizeEdge { } } +#[derive(Debug)] +struct EdgeConstraints { + top_tiled: bool, + #[allow(dead_code)] + top_resizable: bool, + + right_tiled: bool, + #[allow(dead_code)] + right_resizable: bool, + + bottom_tiled: bool, + #[allow(dead_code)] + bottom_resizable: bool, + + left_tiled: bool, + #[allow(dead_code)] + left_resizable: bool, +} + +impl EdgeConstraints { + fn from_atom(atom: u32) -> Self { + EdgeConstraints { + top_tiled: (atom & (1 << 0)) != 0, + top_resizable: (atom & (1 << 1)) != 0, + right_tiled: (atom & (1 << 2)) != 0, + right_resizable: (atom & (1 << 3)) != 0, + bottom_tiled: (atom & (1 << 4)) != 0, + bottom_resizable: (atom & (1 << 5)) != 0, + left_tiled: (atom & (1 << 6)) != 0, + left_resizable: (atom & (1 << 7)) != 0, + } + } + + fn to_tiling(&self) -> Tiling { + Tiling { + top: self.top_tiled, + right: self.right_tiled, + bottom: self.bottom_tiled, + left: self.left_tiled, + } + } +} + #[derive(Debug)] struct Visual { id: xproto::Visualid, @@ -198,6 +241,7 @@ pub struct X11WindowState { active: bool, fullscreen: bool, decorations: WindowDecorations, + edge_constraints: Option, pub handle: AnyWindowHandle, last_insets: [u32; 4], } @@ -497,6 +541,7 @@ impl X11WindowState { destroyed: false, decorations: WindowDecorations::Server, last_insets: [0, 0, 0, 0], + edge_constraints: None, counter_id: sync_request_counter, last_sync_counter: None, refresh_rate, @@ -686,10 +731,32 @@ impl X11WindowStatePtr { pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) { let mut state = self.state.borrow_mut(); - if event.atom == state.atoms._NET_WM_STATE - || event.atom == state.atoms._GTK_EDGE_CONSTRAINTS - { + if event.atom == state.atoms._NET_WM_STATE { self.set_wm_properties(state); + } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { + self.set_edge_constraints(state); + } + } + + fn set_edge_constraints(&self, mut state: std::cell::RefMut) { + let reply = self + .xcb_connection + .get_property( + false, + self.x_window, + state.atoms._GTK_EDGE_CONSTRAINTS, + xproto::AtomEnum::CARDINAL, + 0, + 4, + ) + .unwrap() + .reply() + .unwrap(); + + if reply.value_len != 0 { + let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap()); + let edge_constraints = EdgeConstraints::from_atom(atom); + state.edge_constraints.replace(edge_constraints); } } @@ -1194,15 +1261,19 @@ impl PlatformWindow for X11Window { match state.decorations { WindowDecorations::Server => Decorations::Server, WindowDecorations::Client => { - // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d - Decorations::Client { - tiling: Tiling { + let tiling = if let Some(edge_constraints) = &state.edge_constraints { + edge_constraints.to_tiling() + } else { + // https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d + Tiling { top: state.maximized_vertical, bottom: state.maximized_vertical, left: state.maximized_horizontal, right: state.maximized_horizontal, - }, - } + } + }; + + Decorations::Client { tiling } } } } @@ -1212,17 +1283,26 @@ impl PlatformWindow for X11Window { let dp = (inset.0 * state.scale_factor) as u32; - let (left, right) = if state.maximized_horizontal { - (0, 0) - } else { - (dp, dp) - }; - let (top, bottom) = if state.maximized_vertical { - (0, 0) + let insets = if let Some(edge_constraints) = &state.edge_constraints { + let left = if edge_constraints.left_tiled { 0 } else { dp }; + let top = if edge_constraints.top_tiled { 0 } else { dp }; + let right = if edge_constraints.right_tiled { 0 } else { dp }; + let bottom = if edge_constraints.bottom_tiled { 0 } else { dp }; + + [left, right, top, bottom] } else { - (dp, dp) + let (left, right) = if state.maximized_horizontal { + (0, 0) + } else { + (dp, dp) + }; + let (top, bottom) = if state.maximized_vertical { + (0, 0) + } else { + (dp, dp) + }; + [left, right, top, bottom] }; - let insets = [left, right, top, bottom]; if state.last_insets != insets { state.last_insets = insets; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ba8476fbb4aaa9..c8bcd2c99320b6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6491,6 +6491,8 @@ pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW); } + println!("decorations: {:?}", decorations); + struct GlobalResizeEdge(ResizeEdge); impl Global for GlobalResizeEdge {} From 1bd585186a14a10011133c0f22f794f83d83ac73 Mon Sep 17 00:00:00 2001 From: Zhangfan Date: Fri, 5 Jul 2024 06:32:48 -0700 Subject: [PATCH 38/40] docs: Update the example to set up black formatter in Python (#13839) As titled. The new example is consistent with the instructions in "Configuring Zed". I verified that the example works as expected. Release Notes: - Update the instructions to set up external formatter for Python. --- docs/src/languages/python.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/languages/python.md b/docs/src/languages/python.md index 0af9c5b35f54c0..9bd670b6d410b0 100644 --- a/docs/src/languages/python.md +++ b/docs/src/languages/python.md @@ -58,12 +58,13 @@ A common tool for formatting python code is [Black](https://black.readthedocs.io { "languages": { "Python": { - "format_on_save": { - "external": { + "formatter": { + "external": { "command": "black", "arguments": ["-"] } - } + }, + "format_on_save": "on" } } } From a61188d13732dc87d060edc600d88a3f6a7b3fcb Mon Sep 17 00:00:00 2001 From: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:59:17 +0300 Subject: [PATCH 39/40] ocaml: Pass environment to language server (#13834) Fixed the environment not being passed to ocaml lsp causing it to not work with direnv based installs Release Notes: - N/A --- extensions/ocaml/src/ocaml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ocaml/src/ocaml.rs b/extensions/ocaml/src/ocaml.rs index 6b598730c377bd..b4aebf993d8e97 100644 --- a/extensions/ocaml/src/ocaml.rs +++ b/extensions/ocaml/src/ocaml.rs @@ -26,7 +26,7 @@ impl zed::Extension for OcamlExtension { Ok(zed::Command { command: path, args: Vec::new(), - env: Default::default(), + env: worktree.shell_env(), }) } From 9b7bc04a87f3100c43e9ad036aaa73b73d06d5be Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 5 Jul 2024 10:10:54 -0400 Subject: [PATCH 40/40] ocaml: Bump to v0.0.2 (#13864) This PR bumps the OCaml extension to v0.0.2. Changes: - https://github.com/zed-industries/zed/pull/13834 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/ocaml/Cargo.toml | 2 +- extensions/ocaml/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae1fa6a15266eb..0a011f98a32fa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13841,7 +13841,7 @@ dependencies = [ [[package]] name = "zed_ocaml" -version = "0.0.1" +version = "0.0.2" dependencies = [ "zed_extension_api 0.0.6", ] diff --git a/extensions/ocaml/Cargo.toml b/extensions/ocaml/Cargo.toml index d93b988f387d82..5e0103ed918eac 100644 --- a/extensions/ocaml/Cargo.toml +++ b/extensions/ocaml/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_ocaml" -version = "0.0.1" +version = "0.0.2" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/ocaml/extension.toml b/extensions/ocaml/extension.toml index c5af21a055d001..99f83d126307c2 100644 --- a/extensions/ocaml/extension.toml +++ b/extensions/ocaml/extension.toml @@ -1,7 +1,7 @@ id = "ocaml" name = "OCaml" description = "OCaml support." -version = "0.0.1" +version = "0.0.2" schema_version = 1 authors = ["Rashid Almheiri <69181766+huwaireb@users.noreply.github.com>"] repository = "https://github.com/zed-industries/zed"