From 80080a43e4ad52a3596b69346e97005d21f48500 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 May 2023 10:09:55 -0600 Subject: [PATCH 001/101] Start on an assistant panel based on multi-buffers Each message is represented as a multibuffer excerpt to allow for fluid editing of the conversation transcript. Co-Authored-By: Antonio Scandurra --- Cargo.lock | 4 + assets/icons/speech_bubble_12.svg | 4 +- assets/keymaps/default.json | 34 ++-- crates/ai/Cargo.toml | 4 + crates/ai/src/ai.rs | 46 +++-- crates/ai/src/assistant.rs | 316 ++++++++++++++++++++++++++++++ crates/zed/src/zed.rs | 7 +- 7 files changed, 381 insertions(+), 34 deletions(-) create mode 100644 crates/ai/src/assistant.rs diff --git a/Cargo.lock b/Cargo.lock index 95fcf2224d88ec6fe94a0d842fefec6252b03aa5..27c3a2fdb1768704f663bb0288acc56f53300154 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,9 +106,13 @@ dependencies = [ "futures 0.3.28", "gpui", "isahc", + "language", + "search", "serde", "serde_json", + "theme", "util", + "workspace", ] [[package]] diff --git a/assets/icons/speech_bubble_12.svg b/assets/icons/speech_bubble_12.svg index f5f330056a34f1261d31416b688bcc86dcdf8bf1..736f39a9840022eb882f8473710e73e8228e50ea 100644 --- a/assets/icons/speech_bubble_12.svg +++ b/assets/icons/speech_bubble_12.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 35182dfaa66763cb2ec885ba762b0d9f9fd41b31..5102b7408bd79ff017a57554b57487c502813fbf 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -189,16 +189,16 @@ } }, { - "context": "Editor && extension == zmd", + "context": "Editor && mode == auto_height", "bindings": { - "cmd-enter": "ai::Assist" + "alt-enter": "editor::Newline", + "cmd-alt-enter": "editor::NewlineBelow" } }, { - "context": "Editor && mode == auto_height", + "context": "ContextEditor > Editor", "bindings": { - "alt-enter": "editor::Newline", - "cmd-alt-enter": "editor::NewlineBelow" + "cmd-enter": "assistant::Assist" } }, { @@ -375,27 +375,39 @@ ], "cmd-b": [ "workspace::ToggleLeftDock", - { "focus": true } + { + "focus": true + } ], "cmd-shift-b": [ "workspace::ToggleLeftDock", - { "focus": false } + { + "focus": false + } ], "cmd-r": [ "workspace::ToggleRightDock", - { "focus": true } + { + "focus": true + } ], "cmd-shift-r": [ "workspace::ToggleRightDock", - { "focus": false } + { + "focus": false + } ], "cmd-j": [ "workspace::ToggleBottomDock", - { "focus": true } + { + "focus": true + } ], "cmd-shift-j": [ "workspace::ToggleBottomDock", - { "focus": false } + { + "focus": false + } ], "cmd-shift-f": "workspace::NewSearch", "cmd-k cmd-t": "theme_selector::Toggle", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index b367a4d43cac845950dc123e66ed0c7be15da1f2..14817916f49943a4c5c75965a3c92b0ce38a96b5 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -13,7 +13,11 @@ assets = { path = "../assets"} collections = { path = "../collections"} editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } +search = { path = "../search" } +theme = { path = "../theme" } util = { path = "../util" } +workspace = { path = "../workspace" } serde.workspace = true serde_json.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index c68f41c6bf1cc39d20cc2a9f94dec899a5854516..cd42ee115328febc04e632e3632ad9030ad3edcd 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,3 +1,5 @@ +mod assistant; + use anyhow::{anyhow, Result}; use assets::Assets; use collections::HashMap; @@ -16,6 +18,8 @@ use std::{io, sync::Arc}; use util::channel::{ReleaseChannel, RELEASE_CHANNEL}; use util::{ResultExt, TryFutureExt}; +pub use assistant::AssistantPanel; + actions!(ai, [Assist]); // Data types for chat completion requests @@ -38,7 +42,7 @@ struct ResponseMessage { content: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] enum Role { User, @@ -86,25 +90,27 @@ struct OpenAIChoice { } pub fn init(cx: &mut AppContext) { - if *RELEASE_CHANNEL == ReleaseChannel::Stable { - return; - } - - let assistant = Rc::new(Assistant::default()); - cx.add_action({ - let assistant = assistant.clone(); - move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext| { - assistant.assist(editor, cx).log_err(); - } - }); - cx.capture_action({ - let assistant = assistant.clone(); - move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext| { - if !assistant.cancel_last_assist(cx.view_id()) { - cx.propagate_action(); - } - } - }); + // if *RELEASE_CHANNEL == ReleaseChannel::Stable { + // return; + // } + + assistant::init(cx); + + // let assistant = Rc::new(Assistant::default()); + // cx.add_action({ + // let assistant = assistant.clone(); + // move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext| { + // assistant.assist(editor, cx).log_err(); + // } + // }); + // cx.capture_action({ + // let assistant = assistant.clone(); + // move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext| { + // if !assistant.cancel_last_assist(cx.view_id()) { + // cx.propagate_action(); + // } + // } + // }); } type CompletionId = usize; diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs new file mode 100644 index 0000000000000000000000000000000000000000..ef202e72144d490ccfe713204c7c0aed0d1de04a --- /dev/null +++ b/crates/ai/src/assistant.rs @@ -0,0 +1,316 @@ +use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use futures::StreamExt; +use gpui::{ + actions, elements::*, Action, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, +}; +use language::{language_settings::SoftWrap, Anchor, Buffer}; +use std::sync::Arc; +use util::ResultExt; +use workspace::{ + dock::{DockPosition, Panel}, + item::Item, + pane, Pane, Workspace, +}; + +actions!(assistant, [NewContext, Assist]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(ContextEditor::assist); +} + +pub enum AssistantPanelEvent { + ZoomIn, + ZoomOut, + Focus, + Close, +} + +pub struct AssistantPanel { + width: Option, + pane: ViewHandle, + workspace: WeakViewHandle, + _subscriptions: Vec, +} + +impl AssistantPanel { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let weak_self = cx.weak_handle(); + let pane = cx.add_view(|cx| { + let window_id = cx.window_id(); + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.app_state().background_actions, + Default::default(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(false, cx); + pane.on_can_drop(move |_, cx| false); + pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + let this = weak_self.clone(); + Flex::row() + .with_child(Pane::render_tab_bar_button( + 0, + "icons/plus_12.svg", + Some(("New Context".into(), Some(Box::new(NewContext)))), + cx, + move |_, cx| {}, + None, + )) + .with_child(Pane::render_tab_bar_button( + 1, + if pane.is_zoomed() { + "icons/minimize_8.svg" + } else { + "icons/maximize_8.svg" + }, + Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), + cx, + move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + None, + )) + .into_any() + }); + let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + pane + }); + let subscriptions = vec![ + cx.observe(&pane, |_, _, cx| cx.notify()), + cx.subscribe(&pane, Self::handle_pane_event), + ]; + + Self { + pane, + workspace: workspace.weak_handle(), + width: None, + _subscriptions: subscriptions, + } + } + + fn handle_pane_event( + &mut self, + _pane: ViewHandle, + event: &pane::Event, + cx: &mut ViewContext, + ) { + match event { + pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn), + pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut), + pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus), + pane::Event::Remove => cx.emit(AssistantPanelEvent::Close), + _ => {} + } + } +} + +impl Entity for AssistantPanel { + type Event = AssistantPanelEvent; +} + +impl View for AssistantPanel { + fn ui_name() -> &'static str { + "AssistantPanel" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(&self.pane, cx).into_any() + } +} + +impl Panel for AssistantPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + DockPosition::Right + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) {} + + fn size(&self, cx: &WindowContext) -> f32 { + self.width.unwrap_or(480.) + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + self.width = Some(size); + cx.notify(); + } + + fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::ZoomIn) + } + + fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::ZoomOut) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + if active && self.pane.read(cx).items_len() == 0 { + cx.defer(|this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + let focus = this.pane.read(cx).has_focus(); + let editor = Box::new(cx.add_view(|cx| ContextEditor::new(cx))); + Pane::add_item(workspace, &this.pane, editor, true, focus, None, cx); + }) + } + }); + } + } + + fn icon_path(&self) -> &'static str { + "icons/speech_bubble_12.svg" + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Assistant Panel".into(), None) + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + false + } + + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + + fn should_close_on_event(event: &AssistantPanelEvent) -> bool { + matches!(event, AssistantPanelEvent::Close) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).has_focus() + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, AssistantPanelEvent::Focus) + } +} + +struct ContextEditor { + messages: Vec, + editor: ViewHandle, +} + +impl ContextEditor { + fn new(cx: &mut ViewContext) -> Self { + let messages = vec![Message { + role: Role::User, + content: cx.add_model(|cx| Buffer::new(0, "", cx)), + }]; + + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + for message in &messages { + multibuffer.push_excerpts_with_context_lines( + message.content.clone(), + vec![Anchor::MIN..Anchor::MAX], + 0, + cx, + ); + } + multibuffer + }); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_multibuffer(multibuffer, None, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor + }); + + Self { messages, editor } + } + + fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + let messages = self + .messages + .iter() + .map(|message| RequestMessage { + role: message.role, + content: message.content.read(cx).text(), + }) + .collect(); + let request = OpenAIRequest { + model: "gpt-3.5-turbo".into(), + messages, + stream: true, + }; + + if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { + let stream = stream_completion(api_key, cx.background_executor().clone(), request); + let content = cx.add_model(|cx| Buffer::new(0, "", cx)); + self.messages.push(Message { + role: Role::Assistant, + content: content.clone(), + }); + self.editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.push_excerpts_with_context_lines( + content.clone(), + vec![Anchor::MIN..Anchor::MAX], + 0, + cx, + ); + }); + }); + cx.spawn(|_, mut cx| async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + content.update(&mut cx, |content, cx| { + let text: Arc = choice.delta.content?.into(); + content.edit([(content.len()..content.len(), text)], None, cx); + Some(()) + }); + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } +} + +impl Entity for ContextEditor { + type Event = (); +} + +impl View for ContextEditor { + fn ui_name() -> &'static str { + "ContextEditor" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(&self.editor, cx).into_any() + } +} + +impl Item for ContextEditor { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &gpui::AppContext, + ) -> AnyElement { + Label::new("New Context", style.label.clone()).into_any() + } +} + +struct Message { + role: Role, + content: ModelHandle, +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 619dd81a80407faf2f001fb4d47449bee98938bf..24b7d6356b89a9b691170fa28ed9c31c0bc1606b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,6 +2,7 @@ pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; +use ai::AssistantPanel; use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; @@ -357,7 +358,11 @@ pub fn initialize_workspace( workspace.toggle_dock(project_panel_position, false, cx); } - workspace.add_panel(terminal_panel, cx) + workspace.add_panel(terminal_panel, cx); + + // TODO: deserialize state. + let assistant_panel = cx.add_view(|cx| AssistantPanel::new(workspace, cx)); + workspace.add_panel(assistant_panel, cx); })?; Ok(()) }) From 8f6e67f440082e8a9807e5f0189556f922fb3183 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 May 2023 14:49:27 -0600 Subject: [PATCH 002/101] Cancel assists on escape --- assets/keymaps/default.json | 3 +- crates/ai/src/assistant.rs | 73 +++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 5102b7408bd79ff017a57554b57487c502813fbf..0b89969c70806752dcfb1b15ba48f6f4fe91868b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -198,7 +198,8 @@ { "context": "ContextEditor > Editor", "bindings": { - "cmd-enter": "assistant::Assist" + "cmd-enter": "assistant::Assist", + "escape": "assistant::CancelLastAssist" } }, { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ef202e72144d490ccfe713204c7c0aed0d1de04a..0c58f7a4791243b60d0fda3f66c202003e54c438 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,23 +1,24 @@ use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; -use editor::{Editor, ExcerptRange, MultiBuffer}; +use editor::{Editor, MultiBuffer}; use futures::StreamExt; use gpui::{ - actions, elements::*, Action, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, - ViewHandle, WeakViewHandle, WindowContext, + actions, elements::*, Action, AppContext, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::{language_settings::SoftWrap, Anchor, Buffer}; use std::sync::Arc; -use util::ResultExt; +use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::Item, pane, Pane, Workspace, }; -actions!(assistant, [NewContext, Assist]); +actions!(assistant, [NewContext, Assist, CancelLastAssist]); pub fn init(cx: &mut AppContext) { cx.add_action(ContextEditor::assist); + cx.add_action(ContextEditor::cancel_last_assist); } pub enum AssistantPanelEvent { @@ -203,6 +204,13 @@ impl Panel for AssistantPanel { struct ContextEditor { messages: Vec, editor: ViewHandle, + completion_count: usize, + pending_completions: Vec, +} + +struct PendingCompletion { + id: usize, + task: Task>, } impl ContextEditor { @@ -230,7 +238,12 @@ impl ContextEditor { editor }); - Self { messages, editor } + Self { + messages, + editor, + completion_count: 0, + pending_completions: Vec::new(), + } } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { @@ -265,22 +278,42 @@ impl ContextEditor { ); }); }); - cx.spawn(|_, mut cx| async move { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - content.update(&mut cx, |content, cx| { - let text: Arc = choice.delta.content?.into(); - content.edit([(content.len()..content.len(), text)], None, cx); - Some(()) - }); + let task = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + content.update(&mut cx, |content, cx| { + let text: Arc = choice.delta.content?.into(); + content.edit([(content.len()..content.len(), text)], None, cx); + Some(()) + }); + } } + + this.update(&mut cx, |this, _| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + }) + .ok(); + + anyhow::Ok(()) } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + .log_err() + }); + + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + task, + }); + } + } + + fn cancel_last_assist(&mut self, _: &CancelLastAssist, cx: &mut ViewContext) { + if self.pending_completions.pop().is_none() { + cx.propagate_action(); } } } From 3904971bd80b9351cd7b6c2a5e02e83e19fbcc3b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 May 2023 15:38:03 -0600 Subject: [PATCH 003/101] Hide assistant gutter --- crates/ai/src/ai.rs | 1 - crates/ai/src/assistant.rs | 8 +++++++- crates/editor/src/editor.rs | 9 +++++++++ crates/editor/src/element.rs | 2 +- crates/theme/src/theme.rs | 6 ++++++ styles/src/styleTree/app.ts | 2 ++ styles/src/styleTree/assistant.ts | 13 +++++++++++++ 7 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 styles/src/styleTree/assistant.ts diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index cd42ee115328febc04e632e3632ad9030ad3edcd..64d8b6962801cb57a4d2c253e63e3aac05c14a07 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -15,7 +15,6 @@ use std::cell::RefCell; use std::fs; use std::rc::Rc; use std::{io, sync::Arc}; -use util::channel::{ReleaseChannel, RELEASE_CHANNEL}; use util::{ResultExt, TryFutureExt}; pub use assistant::AssistantPanel; diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0c58f7a4791243b60d0fda3f66c202003e54c438..72e4e9fa427b22bcd985535e5ae431b52029f0f5 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -235,6 +235,7 @@ impl ContextEditor { let editor = cx.add_view(|cx| { let mut editor = Editor::for_multibuffer(multibuffer, None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_show_gutter(false, cx); editor }); @@ -328,7 +329,12 @@ impl View for ContextEditor { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.editor, cx).into_any() + let theme = &theme::current(cx).assistant; + + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container) + .into_any() } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f5d109e15bd2138fba6914e058b575a5bdf42e80..31df5069db39f2c769f7d96b88958b4d9f606c2d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -496,6 +496,7 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + show_gutter: bool, placeholder_text: Option>, highlighted_rows: Option>, #[allow(clippy::type_complexity)] @@ -526,6 +527,7 @@ pub struct Editor { pub struct EditorSnapshot { pub mode: EditorMode, + pub show_gutter: bool, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, is_focused: bool, @@ -1297,6 +1299,7 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + show_gutter: mode == EditorMode::Full, placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), @@ -1393,6 +1396,7 @@ impl Editor { pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { EditorSnapshot { mode: self.mode, + show_gutter: self.show_gutter, display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), @@ -6654,6 +6658,11 @@ impl Editor { cx.notify(); } + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_gutter = show_gutter; + cx.notify(); + } + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6be065084814e570a0411ffb91df4a4eb380e1ac..5b4da4407360b1a2af410b411bc910385307401e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1899,7 +1899,7 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; let gutter_margin; - if snapshot.mode == EditorMode::Full { + if snapshot.show_gutter { let em_width = style.text.em_width(cx.font_cache()); gutter_padding = (em_width * style.gutter_padding_factor).round(); gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b1c9e9c215324bbd6dcdf363d2aeb272ec86f057..010d6956eb47ac57b8a9d08b876715f86a1d61b8 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -60,6 +60,7 @@ pub struct Theme { pub incoming_call_notification: IncomingCallNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, + pub assistant: AssistantStyle, pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub color_scheme: ColorScheme, @@ -967,6 +968,11 @@ pub struct TerminalStyle { pub dim_foreground: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct AssistantStyle { + pub container: ContainerStyle, +} + #[derive(Clone, Deserialize, Default)] pub struct FeedbackStyle { pub submit_button: Interactive, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 6238e1abe1e07a8df61094c82c6aacd11ac32548..a9700a8d9994f0b8f63b74862b8db26c873a37da 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -22,6 +22,7 @@ import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" import welcome from "./welcome" import copilot from "./copilot" +import assistant from "./assistant" export default function app(colorScheme: ColorScheme): Object { return { @@ -50,6 +51,7 @@ export default function app(colorScheme: ColorScheme): Object { simpleMessageNotification: simpleMessageNotification(colorScheme), tooltip: tooltip(colorScheme), terminal: terminal(colorScheme), + assistant: assistant(colorScheme), feedback: feedback(colorScheme), colorScheme: { ...colorScheme, diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..810831285e0ec38f248ae01b3b00ba10c4ca669c --- /dev/null +++ b/styles/src/styleTree/assistant.ts @@ -0,0 +1,13 @@ +import { ColorScheme } from "../themes/common/colorScheme" +import editor from "./editor" + +export default function assistant(colorScheme: ColorScheme) { + return { + container: { + background: editor(colorScheme).background, + padding: { + left: 10, + } + } + } +} From ffbfbe422b6a2c3a43013f9d90e30bb430db5650 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 26 May 2023 16:11:58 -0600 Subject: [PATCH 004/101] WIP: Not sure I actually want to rip this out --- crates/ai/src/ai.rs | 241 +----------------- crates/ai/src/assistant.rs | 128 +++++++--- crates/zed/src/languages/markdown/config.toml | 2 +- 3 files changed, 101 insertions(+), 270 deletions(-) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 64d8b6962801cb57a4d2c253e63e3aac05c14a07..89999f26f39d63591cee4c43274d441439ea5f77 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,21 +1,7 @@ mod assistant; -use anyhow::{anyhow, Result}; -use assets::Assets; -use collections::HashMap; -use editor::Editor; -use futures::AsyncBufReadExt; -use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt}; -use gpui::executor::Background; -use gpui::{actions, AppContext, Task, ViewContext}; -use isahc::prelude::*; -use isahc::{http::StatusCode, Request}; +use gpui::{actions, AppContext}; use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::fs; -use std::rc::Rc; -use std::{io, sync::Arc}; -use util::{ResultExt, TryFutureExt}; pub use assistant::AssistantPanel; @@ -89,230 +75,5 @@ struct OpenAIChoice { } pub fn init(cx: &mut AppContext) { - // if *RELEASE_CHANNEL == ReleaseChannel::Stable { - // return; - // } - assistant::init(cx); - - // let assistant = Rc::new(Assistant::default()); - // cx.add_action({ - // let assistant = assistant.clone(); - // move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext| { - // assistant.assist(editor, cx).log_err(); - // } - // }); - // cx.capture_action({ - // let assistant = assistant.clone(); - // move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext| { - // if !assistant.cancel_last_assist(cx.view_id()) { - // cx.propagate_action(); - // } - // } - // }); -} - -type CompletionId = usize; - -#[derive(Default)] -struct Assistant(RefCell); - -#[derive(Default)] -struct AssistantState { - assist_stacks: HashMap>)>>, - next_completion_id: CompletionId, -} - -impl Assistant { - fn assist(self: &Rc, editor: &mut Editor, cx: &mut ViewContext) -> Result<()> { - let api_key = std::env::var("OPENAI_API_KEY")?; - - let selections = editor.selections.all(cx); - let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| { - // Insert markers around selected text as described in the system prompt above. - let snapshot = buffer.snapshot(cx); - let mut user_message = String::new(); - let mut user_message_suffix = String::new(); - let mut buffer_offset = 0; - for selection in selections { - if !selection.is_empty() { - if user_message_suffix.is_empty() { - user_message_suffix.push_str("\n\n"); - } - user_message_suffix.push_str("[Selected excerpt from above]\n"); - user_message_suffix - .extend(snapshot.text_for_range(selection.start..selection.end)); - user_message_suffix.push_str("\n\n"); - } - - user_message.extend(snapshot.text_for_range(buffer_offset..selection.start)); - user_message.push_str("[SELECTION_START]"); - user_message.extend(snapshot.text_for_range(selection.start..selection.end)); - buffer_offset = selection.end; - user_message.push_str("[SELECTION_END]"); - } - if buffer_offset < snapshot.len() { - user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len())); - } - user_message.push_str(&user_message_suffix); - - // Ensure the document ends with 4 trailing newlines. - let trailing_newline_count = snapshot - .reversed_chars_at(snapshot.len()) - .take_while(|c| *c == '\n') - .take(4); - let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count()); - buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx); - - let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing. - let insertion_site = snapshot.anchor_after(snapshot.len() - 2); - - (user_message, insertion_site) - }); - - let this = self.clone(); - let buffer = editor.buffer().clone(); - let executor = cx.background_executor().clone(); - let editor_id = cx.view_id(); - let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id); - let assist_task = cx.spawn(|_, mut cx| { - async move { - // TODO: We should have a get_string method on assets. This is repateated elsewhere. - let content = Assets::get("contexts/system.zmd").unwrap(); - let mut system_message = std::str::from_utf8(content.data.as_ref()) - .unwrap() - .to_string(); - - if let Ok(custom_system_message_path) = - std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH") - { - system_message.push_str( - "\n\nAlso consider the following user-defined system prompt:\n\n", - ); - // TODO: Replace this with our file system trait object. - system_message.push_str( - &cx.background() - .spawn(async move { fs::read_to_string(custom_system_message_path) }) - .await?, - ); - } - - let stream = stream_completion( - api_key, - executor, - OpenAIRequest { - model: "gpt-4".to_string(), - messages: vec![ - RequestMessage { - role: Role::System, - content: system_message.to_string(), - }, - RequestMessage { - role: Role::User, - content: user_message, - }, - ], - stream: false, - }, - ); - - let mut messages = stream.await?; - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - buffer.update(&mut cx, |buffer, cx| { - let text: Arc = choice.delta.content?.into(); - buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx); - Some(()) - }); - } - } - - this.0 - .borrow_mut() - .assist_stacks - .get_mut(&editor_id) - .unwrap() - .retain(|(id, _)| *id != assist_id); - - anyhow::Ok(()) - } - .log_err() - }); - - self.0 - .borrow_mut() - .assist_stacks - .entry(cx.view_id()) - .or_default() - .push((dbg!(assist_id), assist_task)); - - Ok(()) - } - - fn cancel_last_assist(self: &Rc, editor_id: usize) -> bool { - self.0 - .borrow_mut() - .assist_stacks - .get_mut(&editor_id) - .and_then(|assists| assists.pop()) - .is_some() - } -} - -async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; - - let (tx, rx) = futures::channel::mpsc::unbounded::>(); - - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post("https://api.openai.com/v1/chat/completions") - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; - - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); - - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) - } else { - Ok(None) - } - } - - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - tx.unbounded_send(event).log_err(); - } - } - - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )) - } } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 72e4e9fa427b22bcd985535e5ae431b52029f0f5..e2796f4ecfb656a748dc97958476c54f8c113547 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,12 +1,14 @@ -use crate::{stream_completion, OpenAIRequest, RequestMessage, Role}; +use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role}; +use anyhow::{anyhow, Result}; use editor::{Editor, MultiBuffer}; -use futures::StreamExt; +use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ - actions, elements::*, Action, AppContext, Entity, ModelHandle, Subscription, Task, View, - ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, elements::*, executor::Background, Action, AppContext, Entity, ModelHandle, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; +use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Anchor, Buffer}; -use std::sync::Arc; +use std::{io, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -17,8 +19,8 @@ use workspace::{ actions!(assistant, [NewContext, Assist, CancelLastAssist]); pub fn init(cx: &mut AppContext) { - cx.add_action(ContextEditor::assist); - cx.add_action(ContextEditor::cancel_last_assist); + cx.add_action(Assistant::assist); + cx.capture_action(Assistant::cancel_last_assist); } pub enum AssistantPanelEvent { @@ -37,9 +39,7 @@ pub struct AssistantPanel { impl AssistantPanel { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let weak_self = cx.weak_handle(); let pane = cx.add_view(|cx| { - let window_id = cx.window_id(); let mut pane = Pane::new( workspace.weak_handle(), workspace.app_state().background_actions, @@ -48,16 +48,15 @@ impl AssistantPanel { ); pane.set_can_split(false, cx); pane.set_can_navigate(false, cx); - pane.on_can_drop(move |_, cx| false); + pane.on_can_drop(move |_, _| false); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - let this = weak_self.clone(); Flex::row() .with_child(Pane::render_tab_bar_button( 0, "icons/plus_12.svg", Some(("New Context".into(), Some(Box::new(NewContext)))), cx, - move |_, cx| {}, + move |_, _| todo!(), None, )) .with_child(Pane::render_tab_bar_button( @@ -123,7 +122,7 @@ impl View for AssistantPanel { } impl Panel for AssistantPanel { - fn position(&self, cx: &WindowContext) -> DockPosition { + fn position(&self, _: &WindowContext) -> DockPosition { DockPosition::Right } @@ -131,9 +130,11 @@ impl Panel for AssistantPanel { matches!(position, DockPosition::Right) } - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) {} + fn set_position(&mut self, _: DockPosition, _: &mut ViewContext) { + // TODO! + } - fn size(&self, cx: &WindowContext) -> f32 { + fn size(&self, _: &WindowContext) -> f32 { self.width.unwrap_or(480.) } @@ -164,7 +165,7 @@ impl Panel for AssistantPanel { if let Some(workspace) = this.workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { let focus = this.pane.read(cx).has_focus(); - let editor = Box::new(cx.add_view(|cx| ContextEditor::new(cx))); + let editor = Box::new(cx.add_view(|cx| Assistant::new(cx))); Pane::add_item(workspace, &this.pane, editor, true, focus, None, cx); }) } @@ -180,7 +181,8 @@ impl Panel for AssistantPanel { ("Assistant Panel".into(), None) } - fn should_change_position_on_event(event: &Self::Event) -> bool { + fn should_change_position_on_event(_: &Self::Event) -> bool { + // TODO! false } @@ -201,7 +203,7 @@ impl Panel for AssistantPanel { } } -struct ContextEditor { +struct Assistant { messages: Vec, editor: ViewHandle, completion_count: usize, @@ -210,10 +212,10 @@ struct ContextEditor { struct PendingCompletion { id: usize, - task: Task>, + _task: Task>, } -impl ContextEditor { +impl Assistant { fn new(cx: &mut ViewContext) -> Self { let messages = vec![Message { role: Role::User, @@ -264,15 +266,26 @@ impl ContextEditor { if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { let stream = stream_completion(api_key, cx.background_executor().clone(), request); - let content = cx.add_model(|cx| Buffer::new(0, "", cx)); + let response_buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); self.messages.push(Message { role: Role::Assistant, - content: content.clone(), + content: response_buffer.clone(), + }); + let next_request_buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + self.messages.push(Message { + role: Role::User, + content: next_request_buffer.clone(), }); self.editor.update(cx, |editor, cx| { editor.buffer().update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( - content.clone(), + response_buffer.clone(), + vec![Anchor::MIN..Anchor::MAX], + 0, + cx, + ); + multibuffer.push_excerpts_with_context_lines( + next_request_buffer, vec![Anchor::MIN..Anchor::MAX], 0, cx, @@ -286,7 +299,7 @@ impl ContextEditor { while let Some(message) = messages.next().await { let mut message = message?; if let Some(choice) = message.choices.pop() { - content.update(&mut cx, |content, cx| { + response_buffer.update(&mut cx, |content, cx| { let text: Arc = choice.delta.content?.into(); content.edit([(content.len()..content.len(), text)], None, cx); Some(()) @@ -307,23 +320,23 @@ impl ContextEditor { self.pending_completions.push(PendingCompletion { id: post_inc(&mut self.completion_count), - task, + _task: task, }); } } - fn cancel_last_assist(&mut self, _: &CancelLastAssist, cx: &mut ViewContext) { + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { if self.pending_completions.pop().is_none() { cx.propagate_action(); } } } -impl Entity for ContextEditor { +impl Entity for Assistant { type Event = (); } -impl View for ContextEditor { +impl View for Assistant { fn ui_name() -> &'static str { "ContextEditor" } @@ -338,7 +351,7 @@ impl View for ContextEditor { } } -impl Item for ContextEditor { +impl Item for Assistant { fn tab_content( &self, _: Option, @@ -353,3 +366,60 @@ struct Message { role: Role, content: ModelHandle, } + +async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post("https://api.openai.com/v1/chat/completions") + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + tx.unbounded_send(event).log_err(); + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )) + } +} diff --git a/crates/zed/src/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml index 55204cc7a57ad051004a4fc0d76746057908aa20..2fa3ff3cf2aba297517494cbd1f2e0608daaa402 100644 --- a/crates/zed/src/languages/markdown/config.toml +++ b/crates/zed/src/languages/markdown/config.toml @@ -1,5 +1,5 @@ name = "Markdown" -path_suffixes = ["md", "mdx", "zmd"] +path_suffixes = ["md", "mdx"] brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, From 404bebab63d16c92b76c8920585fde8a79cc58fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 29 May 2023 11:20:24 +0200 Subject: [PATCH 005/101] Set markdown as the assistant's buffer languages --- crates/ai/src/assistant.rs | 243 ++++++++++++++++++++----------------- crates/zed/src/zed.rs | 7 +- 2 files changed, 136 insertions(+), 114 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e2796f4ecfb656a748dc97958476c54f8c113547..1fa9c3cc4ffadfc2b7437a562ba0e175db1e6f78 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -3,11 +3,11 @@ use anyhow::{anyhow, Result}; use editor::{Editor, MultiBuffer}; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ - actions, elements::*, executor::Background, Action, AppContext, Entity, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity, + ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use isahc::{http::StatusCode, Request, RequestExt}; -use language::{language_settings::SoftWrap, Anchor, Buffer}; +use language::{language_settings::SoftWrap, Anchor, Buffer, Language, LanguageRegistry}; use std::{io, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ @@ -38,57 +38,70 @@ pub struct AssistantPanel { } impl AssistantPanel { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let pane = cx.add_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.app_state().background_actions, - Default::default(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.on_can_drop(move |_, _| false); - pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - Flex::row() - .with_child(Pane::render_tab_bar_button( - 0, - "icons/plus_12.svg", - Some(("New Context".into(), Some(Box::new(NewContext)))), - cx, - move |_, _| todo!(), - None, - )) - .with_child(Pane::render_tab_bar_button( - 1, - if pane.is_zoomed() { - "icons/minimize_8.svg" - } else { - "icons/maximize_8.svg" - }, - Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), - cx, - move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - None, - )) - .into_any() - }); - let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); - pane - }); - let subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - - Self { - pane, - workspace: workspace.weak_handle(), - width: None, - _subscriptions: subscriptions, - } + pub fn load( + workspace: WeakViewHandle, + cx: AsyncAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // TODO: deserialize state. + workspace.update(&mut cx, |workspace, cx| { + cx.add_view(|cx| { + let pane = cx.add_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.app_state().background_actions, + Default::default(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(false, cx); + pane.on_can_drop(move |_, _| false); + pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + Flex::row() + .with_child(Pane::render_tab_bar_button( + 0, + "icons/plus_12.svg", + Some(("New Context".into(), Some(Box::new(NewContext)))), + cx, + move |_, _| todo!(), + None, + )) + .with_child(Pane::render_tab_bar_button( + 1, + if pane.is_zoomed() { + "icons/minimize_8.svg" + } else { + "icons/maximize_8.svg" + }, + Some(( + "Toggle Zoom".into(), + Some(Box::new(workspace::ToggleZoom)), + )), + cx, + move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + None, + )) + .into_any() + }); + let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); + pane.toolbar() + .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + pane + }); + let subscriptions = vec![ + cx.observe(&pane, |_, _, cx| cx.notify()), + cx.subscribe(&pane, Self::handle_pane_event), + ]; + + Self { + pane, + workspace: workspace.weak_handle(), + width: None, + _subscriptions: subscriptions, + } + }) + }) + }) } fn handle_pane_event( @@ -161,15 +174,28 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active && self.pane.read(cx).items_len() == 0 { - cx.defer(|this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - let focus = this.pane.read(cx).has_focus(); - let editor = Box::new(cx.add_view(|cx| Assistant::new(cx))); - Pane::add_item(workspace, &this.pane, editor, true, focus, None, cx); - }) - } - }); + let workspace = self.workspace.clone(); + let pane = self.pane.clone(); + let focus = self.has_focus(cx); + cx.spawn(|_, mut cx| async move { + let markdown = workspace + .read_with(&cx, |workspace, _| { + workspace + .app_state() + .languages + .language_for_name("Markdown") + })? + .await?; + workspace.update(&mut cx, |workspace, cx| { + let editor = Box::new(cx.add_view(|cx| { + Assistant::new(markdown, workspace.app_state().languages.clone(), cx) + })); + Pane::add_item(workspace, &pane, editor, true, focus, None, cx); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } @@ -208,6 +234,8 @@ struct Assistant { editor: ViewHandle, completion_count: usize, pending_completions: Vec, + markdown: Arc, + language_registry: Arc, } struct PendingCompletion { @@ -216,37 +244,29 @@ struct PendingCompletion { } impl Assistant { - fn new(cx: &mut ViewContext) -> Self { - let messages = vec![Message { - role: Role::User, - content: cx.add_model(|cx| Buffer::new(0, "", cx)), - }]; - - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - for message in &messages { - multibuffer.push_excerpts_with_context_lines( - message.content.clone(), - vec![Anchor::MIN..Anchor::MAX], - 0, - cx, - ); - } - multibuffer - }); + fn new( + markdown: Arc, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { let editor = cx.add_view(|cx| { + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let mut editor = Editor::for_multibuffer(multibuffer, None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); editor }); - Self { - messages, + let mut this = Self { + messages: Default::default(), editor, completion_count: 0, pending_completions: Vec::new(), - } + markdown, + language_registry, + }; + this.push_message(Role::User, cx); + this } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { @@ -266,32 +286,8 @@ impl Assistant { if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { let stream = stream_completion(api_key, cx.background_executor().clone(), request); - let response_buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); - self.messages.push(Message { - role: Role::Assistant, - content: response_buffer.clone(), - }); - let next_request_buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); - self.messages.push(Message { - role: Role::User, - content: next_request_buffer.clone(), - }); - self.editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |multibuffer, cx| { - multibuffer.push_excerpts_with_context_lines( - response_buffer.clone(), - vec![Anchor::MIN..Anchor::MAX], - 0, - cx, - ); - multibuffer.push_excerpts_with_context_lines( - next_request_buffer, - vec![Anchor::MIN..Anchor::MAX], - 0, - cx, - ); - }); - }); + let response_buffer = self.push_message(Role::Assistant, cx); + self.push_message(Role::User, cx); let task = cx.spawn(|this, mut cx| { async move { let mut messages = stream.await?; @@ -330,6 +326,33 @@ impl Assistant { cx.propagate_action(); } } + + fn push_message(&mut self, role: Role, cx: &mut ViewContext) -> ModelHandle { + let content = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.set_language(Some(self.markdown.clone()), cx); + buffer.set_language_registry(self.language_registry.clone()); + buffer + }); + let message = Message { + role, + content: content.clone(), + }; + self.messages.push(message); + + self.editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.push_excerpts_with_context_lines( + content.clone(), + vec![Anchor::MIN..Anchor::MAX], + 0, + cx, + ) + }); + }); + + content + } } impl Entity for Assistant { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 24b7d6356b89a9b691170fa28ed9c31c0bc1606b..4c3f21467fdaef1446fd6b2bbbdfa093a42b40f8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -340,7 +340,9 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; + let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel, assistant_panel) = + futures::try_join!(project_panel, terminal_panel, assistant_panel)?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); @@ -359,9 +361,6 @@ pub fn initialize_workspace( } workspace.add_panel(terminal_panel, cx); - - // TODO: deserialize state. - let assistant_panel = cx.add_view(|cx| AssistantPanel::new(workspace, cx)); workspace.add_panel(assistant_panel, cx); })?; Ok(()) From 52e8bf29285aad456914a4f23b3c714d4d779def Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 29 May 2023 15:57:55 +0200 Subject: [PATCH 006/101] Show custom header for assistant messages --- crates/ai/src/assistant.rs | 167 +++++++++++++------ crates/editor/src/editor.rs | 25 ++- crates/editor/src/element.rs | 267 +++++++++++++++++------------- crates/theme/src/theme.rs | 3 + styles/src/styleTree/assistant.ts | 16 +- 5 files changed, 310 insertions(+), 168 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1fa9c3cc4ffadfc2b7437a562ba0e175db1e6f78..14ac7411146c3256283307f2245517ca9dc6d21a 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,13 +1,15 @@ use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role}; use anyhow::{anyhow, Result}; -use editor::{Editor, MultiBuffer}; +use collections::HashMap; +use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity, - ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use isahc::{http::StatusCode, Request, RequestExt}; -use language::{language_settings::SoftWrap, Anchor, Buffer, Language, LanguageRegistry}; +use language::{language_settings::SoftWrap, Buffer, Language, LanguageRegistry}; use std::{io, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ @@ -19,8 +21,8 @@ use workspace::{ actions!(assistant, [NewContext, Assist, CancelLastAssist]); pub fn init(cx: &mut AppContext) { - cx.add_action(Assistant::assist); - cx.capture_action(Assistant::cancel_last_assist); + cx.add_action(AssistantEditor::assist); + cx.capture_action(AssistantEditor::cancel_last_assist); } pub enum AssistantPanelEvent { @@ -188,7 +190,7 @@ impl Panel for AssistantPanel { .await?; workspace.update(&mut cx, |workspace, cx| { let editor = Box::new(cx.add_view(|cx| { - Assistant::new(markdown, workspace.app_state().languages.clone(), cx) + AssistantEditor::new(markdown, workspace.app_state().languages.clone(), cx) })); Pane::add_item(workspace, &pane, editor, true, focus, None, cx); })?; @@ -230,38 +232,31 @@ impl Panel for AssistantPanel { } struct Assistant { + buffer: ModelHandle, messages: Vec, - editor: ViewHandle, + messages_by_id: HashMap, completion_count: usize, pending_completions: Vec, markdown: Arc, language_registry: Arc, } -struct PendingCompletion { - id: usize, - _task: Task>, +impl Entity for Assistant { + type Event = (); } impl Assistant { fn new( markdown: Arc, language_registry: Arc, - cx: &mut ViewContext, + cx: &mut ModelContext, ) -> Self { - let editor = cx.add_view(|cx| { - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let mut editor = Editor::for_multibuffer(multibuffer, None, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_gutter(false, cx); - editor - }); - let mut this = Self { + buffer: cx.add_model(|_| MultiBuffer::new(0)), messages: Default::default(), - editor, - completion_count: 0, - pending_completions: Vec::new(), + messages_by_id: Default::default(), + completion_count: Default::default(), + pending_completions: Default::default(), markdown, language_registry, }; @@ -269,7 +264,7 @@ impl Assistant { this } - fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + fn assist(&mut self, cx: &mut ModelContext) { let messages = self .messages .iter() @@ -285,8 +280,8 @@ impl Assistant { }; if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { - let stream = stream_completion(api_key, cx.background_executor().clone(), request); - let response_buffer = self.push_message(Role::Assistant, cx); + let stream = stream_completion(api_key, cx.background().clone(), request); + let response = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); let task = cx.spawn(|this, mut cx| { async move { @@ -295,7 +290,7 @@ impl Assistant { while let Some(message) = messages.next().await { let mut message = message?; if let Some(choice) = message.choices.pop() { - response_buffer.update(&mut cx, |content, cx| { + response.content.update(&mut cx, |content, cx| { let text: Arc = choice.delta.content?.into(); content.edit([(content.len()..content.len(), text)], None, cx); Some(()) @@ -306,8 +301,7 @@ impl Assistant { this.update(&mut cx, |this, _| { this.pending_completions .retain(|completion| completion.id != this.completion_count); - }) - .ok(); + }); anyhow::Ok(()) } @@ -321,45 +315,123 @@ impl Assistant { } } - fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if self.pending_completions.pop().is_none() { - cx.propagate_action(); - } + fn cancel_last_assist(&mut self) -> bool { + self.pending_completions.pop().is_some() } - fn push_message(&mut self, role: Role, cx: &mut ViewContext) -> ModelHandle { + fn push_message(&mut self, role: Role, cx: &mut ModelContext) -> Message { let content = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); buffer.set_language(Some(self.markdown.clone()), cx); buffer.set_language_registry(self.language_registry.clone()); buffer }); + let excerpt_id = self.buffer.update(cx, |buffer, cx| { + buffer + .push_excerpts( + content.clone(), + vec![ExcerptRange { + context: 0..0, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + let message = Message { role, content: content.clone(), }; - self.messages.push(message); + self.messages.push(message.clone()); + self.messages_by_id.insert(excerpt_id, message.clone()); + message + } +} - self.editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - buffer.push_excerpts_with_context_lines( - content.clone(), - vec![Anchor::MIN..Anchor::MAX], - 0, - cx, - ) - }); +struct PendingCompletion { + id: usize, + _task: Task>, +} + +struct AssistantEditor { + assistant: ModelHandle, + editor: ViewHandle, +} + +impl AssistantEditor { + fn new( + markdown: Arc, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let assistant = cx.add_model(|cx| Assistant::new(markdown, language_registry, cx)); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx); + editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_show_gutter(false, cx); + editor.set_render_excerpt_header( + { + let assistant = assistant.clone(); + move |editor, params: editor::RenderExcerptHeaderParams, cx| { + let style = &theme::current(cx).assistant; + if let Some(message) = assistant.read(cx).messages_by_id.get(¶ms.id) { + let sender = match message.role { + Role::User => Label::new("You", style.user_sender.text.clone()) + .contained() + .with_style(style.user_sender.container), + Role::Assistant => { + Label::new("Assistant", style.assistant_sender.text.clone()) + .contained() + .with_style(style.assistant_sender.container) + } + Role::System => { + Label::new("System", style.assistant_sender.text.clone()) + .contained() + .with_style(style.assistant_sender.container) + } + }; + + Flex::row() + .with_child(sender) + .aligned() + .left() + .contained() + .with_style(style.header) + .into_any() + } else { + Empty::new().into_any() + } + } + }, + cx, + ); + editor }); + Self { assistant, editor } + } - content + fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + self.assistant + .update(cx, |assistant, cx| assistant.assist(cx)); + } + + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if !self + .assistant + .update(cx, |assistant, _| assistant.cancel_last_assist()) + { + cx.propagate_action(); + } } } -impl Entity for Assistant { +impl Entity for AssistantEditor { type Event = (); } -impl View for Assistant { +impl View for AssistantEditor { fn ui_name() -> &'static str { "ContextEditor" } @@ -374,7 +446,7 @@ impl View for Assistant { } } -impl Item for Assistant { +impl Item for AssistantEditor { fn tab_content( &self, _: Option, @@ -385,6 +457,7 @@ impl Item for Assistant { } } +#[derive(Clone)] struct Message { role: Role, content: ModelHandle, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 31df5069db39f2c769f7d96b88958b4d9f606c2d..a1e55dc03630d3d4136bc1ded66e3fe10ef5c0e9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -46,7 +46,8 @@ use gpui::{ platform::{CursorStyle, MouseButton}, serde_json::{self, json}, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, - ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + LayoutContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -498,6 +499,7 @@ pub struct Editor { mode: EditorMode, show_gutter: bool, placeholder_text: Option>, + render_excerpt_header: Option, highlighted_rows: Option>, #[allow(clippy::type_complexity)] background_highlights: BTreeMap Color, Vec>)>, @@ -1301,6 +1303,7 @@ impl Editor { mode, show_gutter: mode == EditorMode::Full, placeholder_text: None, + render_excerpt_header: None, highlighted_rows: None, background_highlights: Default::default(), nav_history: None, @@ -6663,6 +6666,20 @@ impl Editor { cx.notify(); } + pub fn set_render_excerpt_header( + &mut self, + render_excerpt_header: impl 'static + + Fn( + &mut Editor, + RenderExcerptHeaderParams, + &mut LayoutContext, + ) -> AnyElement, + cx: &mut ViewContext, + ) { + self.render_excerpt_header = Some(Arc::new(render_excerpt_header)); + cx.notify(); + } + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { @@ -7308,8 +7325,12 @@ impl View for Editor { }); } + let mut editor = EditorElement::new(style.clone()); + if let Some(render_excerpt_header) = self.render_excerpt_header.clone() { + editor = editor.with_render_excerpt_header(render_excerpt_header); + } Stack::new() - .with_child(EditorElement::new(style.clone())) + .with_child(editor) .with_child(ChildView::new(&self.mouse_context_menu, cx)) .into_any() } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5b4da4407360b1a2af410b411bc910385307401e..a028c7ca1c985aa6ecc1ff4e24430e23a8ffa3b7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -91,18 +91,41 @@ impl SelectionLayout { } } -#[derive(Clone)] +pub struct RenderExcerptHeaderParams<'a> { + pub id: crate::ExcerptId, + pub buffer: &'a language::BufferSnapshot, + pub range: &'a crate::ExcerptRange, + pub starts_new_buffer: bool, + pub gutter_padding: f32, + pub editor_style: &'a EditorStyle, +} + +pub type RenderExcerptHeader = Arc< + dyn Fn( + &mut Editor, + RenderExcerptHeaderParams, + &mut LayoutContext, + ) -> AnyElement, +>; + pub struct EditorElement { style: Arc, + render_excerpt_header: RenderExcerptHeader, } impl EditorElement { pub fn new(style: EditorStyle) -> Self { Self { style: Arc::new(style), + render_excerpt_header: Arc::new(render_excerpt_header), } } + pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self { + self.render_excerpt_header = render; + self + } + fn attach_mouse_handlers( scene: &mut SceneBuilder, position_map: &Arc, @@ -1465,11 +1488,9 @@ impl EditorElement { line_height: f32, style: &EditorStyle, line_layouts: &[LineWithInvisibles], - include_root: bool, editor: &mut Editor, cx: &mut LayoutContext, ) -> (f32, Vec) { - let tooltip_style = theme::current(cx).tooltip.clone(); let scroll_x = snapshot.scroll_anchor.offset.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) @@ -1510,112 +1531,18 @@ impl EditorElement { range, starts_new_buffer, .. - } => { - let id = *id; - let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { - let jump_path = ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }; - let jump_anchor = range - .primary - .as_ref() - .map_or(range.context.start, |primary| primary.start); - let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - - enum JumpIcon {} - MouseEventHandler::::new(id.into(), cx, |state, _| { - let style = style.jump_icon.style_for(state, false); - Svg::new("icons/arrow_up_right_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, editor, cx| { - if let Some(workspace) = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Editor::jump( - workspace, - jump_path.clone(), - jump_position, - jump_anchor, - cx, - ); - }); - } - }) - .with_tooltip::( - id.into(), - "Jump to Buffer".to_string(), - Some(Box::new(crate::OpenExcerpts)), - tooltip_style.clone(), - cx, - ) - .aligned() - .flex_float() - }); - - if *starts_new_buffer { - let style = &self.style.diagnostic_path_header; - let font_size = - (style.text_scale_factor * self.style.text.font_size).round(); - - let path = buffer.resolve_file_path(cx, include_root); - let mut filename = None; - let mut parent_path = None; - // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - if let Some(path) = path { - filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = - path.parent().map(|p| p.to_string_lossy().to_string() + "/"); - } - - Flex::row() - .with_child( - Label::new( - filename.unwrap_or_else(|| "untitled".to_string()), - style.filename.text.clone().with_font_size(font_size), - ) - .contained() - .with_style(style.filename.container) - .aligned(), - ) - .with_children(parent_path.map(|path| { - Label::new(path, style.path.text.clone().with_font_size(font_size)) - .contained() - .with_style(style.path.container) - .aligned() - })) - .with_children(jump_icon) - .contained() - .with_style(style.container) - .with_padding_left(gutter_padding) - .with_padding_right(gutter_padding) - .expanded() - .into_any_named("path header block") - } else { - let text_style = self.style.text.clone(); - Flex::row() - .with_child(Label::new("⋯", text_style)) - .with_children(jump_icon) - .contained() - .with_padding_left(gutter_padding) - .with_padding_right(gutter_padding) - .expanded() - .into_any_named("collapsed context") - } - } + } => (self.render_excerpt_header)( + editor, + RenderExcerptHeaderParams { + id: *id, + buffer, + range, + starts_new_buffer: *starts_new_buffer, + gutter_padding, + editor_style: style, + }, + cx, + ), }; element.layout( @@ -2080,12 +2007,6 @@ impl Element for EditorElement { ShowScrollbar::Never => false, }; - let include_root = editor - .project - .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); - let fold_ranges: Vec<(BufferRow, Range, Color)> = fold_ranges .into_iter() .map(|(id, fold)| { @@ -2144,7 +2065,6 @@ impl Element for EditorElement { line_height, &style, &line_layouts, - include_root, editor, cx, ); @@ -2759,6 +2679,121 @@ impl HighlightedRange { } } +fn render_excerpt_header( + editor: &mut Editor, + RenderExcerptHeaderParams { + id, + buffer, + range, + starts_new_buffer, + gutter_padding, + editor_style, + }: RenderExcerptHeaderParams, + cx: &mut LayoutContext, +) -> AnyElement { + let tooltip_style = theme::current(cx).tooltip.clone(); + let include_root = editor + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + + enum JumpIcon {} + MouseEventHandler::::new(id.into(), cx, |state, _| { + let style = editor_style.jump_icon.style_for(state, false); + Svg::new("icons/arrow_up_right_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, editor, cx| { + if let Some(workspace) = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx); + }); + } + }) + .with_tooltip::( + id.into(), + "Jump to Buffer".to_string(), + Some(Box::new(crate::OpenExcerpts)), + tooltip_style.clone(), + cx, + ) + .aligned() + .flex_float() + }); + + if starts_new_buffer { + let style = &editor_style.diagnostic_path_header; + let font_size = (style.text_scale_factor * editor_style.text.font_size).round(); + + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + } + + Flex::row() + .with_child( + Label::new( + filename.unwrap_or_else(|| "untitled".to_string()), + style.filename.text.clone().with_font_size(font_size), + ) + .contained() + .with_style(style.filename.container) + .aligned(), + ) + .with_children(parent_path.map(|path| { + Label::new(path, style.path.text.clone().with_font_size(font_size)) + .contained() + .with_style(style.path.container) + .aligned() + })) + .with_children(jump_icon) + .contained() + .with_style(style.container) + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("path header block") + } else { + let text_style = editor_style.text.clone(); + Flex::row() + .with_child(Label::new("⋯", text_style)) + .with_children(jump_icon) + .contained() + .with_padding_left(gutter_padding) + .with_padding_right(gutter_padding) + .expanded() + .into_any_named("collapsed context") + } +} + fn position_to_display_point( position: Vector2F, text_bounds: RectF, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 010d6956eb47ac57b8a9d08b876715f86a1d61b8..0056230766800aa216cd164461471a42bf4772a7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -971,6 +971,9 @@ pub struct TerminalStyle { #[derive(Clone, Deserialize, Default)] pub struct AssistantStyle { pub container: ContainerStyle, + pub header: ContainerStyle, + pub user_sender: ContainedText, + pub assistant_sender: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 810831285e0ec38f248ae01b3b00ba10c4ca669c..ea02c3b383976747010e2a31b32a3ba43833111d 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -1,13 +1,23 @@ import { ColorScheme } from "../themes/common/colorScheme" +import { text, border } from "./components" import editor from "./editor" export default function assistant(colorScheme: ColorScheme) { + const layer = colorScheme.highest; return { container: { background: editor(colorScheme).background, - padding: { - left: 10, - } + padding: { left: 12 } + }, + header: { + border: border(layer, "default", { bottom: true, top: true }), + margin: { bottom: 6, top: 6 } + }, + user_sender: { + ...text(layer, "sans", "default", { size: "sm", weight: "bold" }), + }, + assistant_sender: { + ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), } } } From 890c42a75a4fbe5a83f8fef399befc9de99c6045 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 29 May 2023 16:23:16 +0200 Subject: [PATCH 007/101] Show time in assistant messages --- Cargo.lock | 1 + crates/ai/Cargo.toml | 5 +++-- crates/ai/src/assistant.rs | 14 +++++++++++++- crates/theme/src/theme.rs | 1 + styles/src/styleTree/assistant.ts | 4 ++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27c3a2fdb1768704f663bb0288acc56f53300154..a22db1b35d2c4acdc74c8d1af4603e22a1322158 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assets", + "chrono", "collections", "editor", "futures 0.3.28", diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 14817916f49943a4c5c75965a3c92b0ce38a96b5..861a9e4785e0fd8373dc6d3ef59d23949a19a939 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -19,11 +19,12 @@ theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } -serde.workspace = true -serde_json.workspace = true anyhow.workspace = true +chrono = "0.4" futures.workspace = true isahc.workspace = true +serde.workspace = true +serde_json.workspace = true [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 14ac7411146c3256283307f2245517ca9dc6d21a..c6c5d4d009ae1b89f65acb6738a27ec60a7ec054 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,5 +1,6 @@ use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role}; use anyhow::{anyhow, Result}; +use chrono::{DateTime, Local}; use collections::HashMap; use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -343,6 +344,7 @@ impl Assistant { let message = Message { role, content: content.clone(), + sent_at: Local::now(), }; self.messages.push(message.clone()); self.messages_by_id.insert(excerpt_id, message.clone()); @@ -394,7 +396,16 @@ impl AssistantEditor { }; Flex::row() - .with_child(sender) + .with_child(sender.aligned()) + .with_child( + Label::new( + message.sent_at.format("%I:%M%P").to_string(), + style.sent_at.text.clone(), + ) + .contained() + .with_style(style.sent_at.container) + .aligned(), + ) .aligned() .left() .contained() @@ -461,6 +472,7 @@ impl Item for AssistantEditor { struct Message { role: Role, content: ModelHandle, + sent_at: DateTime, } async fn stream_completion( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0056230766800aa216cd164461471a42bf4772a7..1f99742cfe20cd4770e65457f2462c3f11829cd3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -972,6 +972,7 @@ pub struct TerminalStyle { pub struct AssistantStyle { pub container: ContainerStyle, pub header: ContainerStyle, + pub sent_at: ContainedText, pub user_sender: ContainedText, pub assistant_sender: ContainedText, } diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index ea02c3b383976747010e2a31b32a3ba43833111d..0ff65a22ae3a730e0152bf4995cd26e6599aea57 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -18,6 +18,10 @@ export default function assistant(colorScheme: ColorScheme) { }, assistant_sender: { ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), + }, + sent_at: { + margin: { top: 2, left: 8 }, + ...text(layer, "sans", "default", { size: "2xs" }), } } } From 89446c7fd41f3733b657e4f0a57438d323770331 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 29 May 2023 13:48:27 -0700 Subject: [PATCH 008/101] Start work on respecting project-specific settings --- crates/copilot/src/copilot.rs | 5 + crates/copilot_button/src/copilot_button.rs | 2 +- crates/editor/src/display_map.rs | 2 +- crates/editor/src/items.rs | 4 + crates/editor/src/multi_buffer.rs | 20 ++- crates/language/src/buffer.rs | 17 ++- crates/language/src/language_settings.rs | 13 +- crates/project/src/lsp_command.rs | 3 +- crates/project/src/project.rs | 130 +++++++++++++++----- crates/project/src/project_tests.rs | 56 +++++++++ crates/project/src/worktree.rs | 45 +++++-- crates/settings/src/settings_file.rs | 13 +- crates/settings/src/settings_store.rs | 91 +++++++++----- crates/terminal_view/src/terminal_view.rs | 5 +- crates/util/src/paths.rs | 1 + crates/zed/src/languages/yaml.rs | 2 +- 16 files changed, 326 insertions(+), 83 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index de9104a6848d504eb78d28ab45da896d771dca29..88b0aebd17127765535db8bf8966dd630e95b851 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -787,6 +787,7 @@ impl Copilot { let position = position.to_point_utf16(buffer); let settings = language_settings( buffer.language_at(position).map(|l| l.name()).as_deref(), + buffer.file().map(|f| f.as_ref()), cx, ); let tab_size = settings.tab_size; @@ -1175,6 +1176,10 @@ mod tests { fn to_proto(&self) -> rpc::proto::File { unimplemented!() } + + fn worktree_id(&self) -> usize { + 0 + } } impl language::LocalFile for File { diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 17d27ca41f66caf08117518d29b7f6f627febe4c..686b3d2dfb3dc222433166f3aeed20f5d869be33 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -198,7 +198,7 @@ impl CopilotButton { if let Some(language) = self.language.clone() { let fs = fs.clone(); let language_enabled = - language_settings::language_settings(Some(language.as_ref()), cx) + language_settings::language_settings(Some(language.as_ref()), None, cx) .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 366e47ddc640fa6c9ee205debdc7b837b205ebe8..f8d6f0bc406a497f13e36f320ed461aff1ddec60 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -277,7 +277,7 @@ impl DisplayMap { .as_singleton() .and_then(|buffer| buffer.read(cx).language()) .map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + language_settings(language_name.as_deref(), None, cx).tab_size } #[cfg(test)] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 40e7c89cb298e34510002550349f7faff6f0fa19..8da746075e25f7130b739b82d40e33ceb2ef8130 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1231,6 +1231,10 @@ mod tests { unimplemented!() } + fn worktree_id(&self) -> usize { + 0 + } + fn is_deleted(&self) -> bool { unimplemented!() } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4650dff38f58088eab5fc1638080680dc86dd84e..597feb832b07c80ed33bdf578377c720c16a393e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1377,8 +1377,14 @@ impl MultiBuffer { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(point, cx); - language_settings(language.map(|l| l.name()).as_deref(), cx) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { + let buffer = buffer.read(cx); + language = buffer.language_at(offset).map(|l| l.name()); + file = buffer.file().map(|f| f.as_ref()); + } + language_settings(language.as_deref(), file, cx) } pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { @@ -2785,9 +2791,13 @@ impl MultiBufferSnapshot { point: T, cx: &'a AppContext, ) -> &'a LanguageSettings { - self.point_to_buffer_offset(point) - .map(|(buffer, offset)| buffer.settings_at(offset, cx)) - .unwrap_or_else(|| language_settings(None, cx)) + let mut language = None; + let mut file = None; + if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { + language = buffer.language_at(offset).map(|l| l.name()); + file = buffer.file().map(|f| f.as_ref()); + } + language_settings(language.as_deref(), file, cx) } pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 93b50cf597cfd3e0ed18ac8e317fa5847740f1b1..ddec085e2a622ca84190c5acec9c891512a80b2f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -216,6 +216,11 @@ pub trait File: Send + Sync { /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; + /// Returns the id of the worktree to which this file belongs. + /// + /// This is needed for looking up project-specific settings. + fn worktree_id(&self) -> usize; + fn is_deleted(&self) -> bool; fn as_any(&self) -> &dyn Any; @@ -1803,7 +1808,11 @@ impl BufferSnapshot { pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { let language_name = self.language_at(position).map(|language| language.name()); - let settings = language_settings(language_name.as_deref(), cx); + let settings = language_settings( + language_name.as_deref(), + self.file().map(|f| f.as_ref()), + cx, + ); if settings.hard_tabs { IndentSize::tab() } else { @@ -2128,7 +2137,11 @@ impl BufferSnapshot { cx: &'a AppContext, ) -> &'a LanguageSettings { let language = self.language_at(position); - language_settings(language.map(|l| l.name()).as_deref(), cx) + language_settings( + language.map(|l| l.name()).as_deref(), + self.file.as_ref().map(AsRef::as_ref), + cx, + ) } pub fn language_scope_at(&self, position: D) -> Option { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index c98297c03648f7db9c307a592b4f7bf2dcfe279d..70aaca03c408ab0a9995fee88f54144f4ab997d9 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,3 +1,4 @@ +use crate::File; use anyhow::Result; use collections::HashMap; use globset::GlobMatcher; @@ -13,8 +14,16 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { - settings::get::(cx).language(language) +pub fn language_settings<'a>( + language: Option<&str>, + file: Option<&dyn File>, + cx: &'a AppContext, +) -> &'a LanguageSettings { + settings::get_local::( + file.map(|f| (f.worktree_id(), f.path().as_ref())), + cx, + ) + .language(language) } pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5ee64438963977fb6f265ed4d5fe32444bd96b3f..0a8b5f8a2e52f6a33e14bd5bc4a01ebe3dba7601 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1717,7 +1717,8 @@ impl LspCommand for OnTypeFormatting { let tab_size = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + let file = buffer.file().map(|f| f.as_ref()); + language_settings(language_name.as_deref(), file, cx).tab_size }); Ok(Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 92210a75a8ea2067aafe3e36d68449c3af2e5142..c76587ff56328fabdab93abf77965e547f11979b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -71,7 +71,10 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use terminals::Terminals; -use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; +use util::{ + debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, + ResultExt, TryFutureExt as _, +}; pub use fs::*; pub use worktree::*; @@ -697,12 +700,7 @@ impl Project { .language(Some(&language.name())) .enable_language_server { - let worktree = file.worktree.read(cx); - language_servers_to_start.push(( - worktree.id(), - worktree.as_local().unwrap().abs_path().clone(), - language.clone(), - )); + language_servers_to_start.push((file.worktree.clone(), language.clone())); } } } @@ -732,8 +730,9 @@ impl Project { } // Start all the newly-enabled language servers. - for (worktree_id, worktree_path, language) in language_servers_to_start { - self.start_language_servers(worktree_id, worktree_path, language, cx); + for (worktree, language) in language_servers_to_start { + let worktree_path = worktree.read(cx).abs_path(); + self.start_language_servers(&worktree, worktree_path, language, cx); } if !self.copilot_enabled && Copilot::global(cx).is_some() { @@ -2320,25 +2319,34 @@ impl Project { }); if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if let Some(worktree) = file.worktree.read(cx).as_local() { - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); - self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx); + let worktree = file.worktree.clone(); + if let Some(tree) = worktree.read(cx).as_local() { + self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); } } } fn start_language_servers( &mut self, - worktree_id: WorktreeId, + worktree: &ModelHandle, worktree_path: Arc, language: Arc, cx: &mut ModelContext, ) { - if !language_settings(Some(&language.name()), cx).enable_language_server { + if !language_settings( + Some(&language.name()), + worktree + .update(cx, |tree, cx| tree.root_file(cx)) + .as_ref() + .map(|f| f as _), + cx, + ) + .enable_language_server + { return; } + let worktree_id = worktree.read(cx).id(); for adapter in language.lsp_adapters() { let key = (worktree_id, adapter.name.clone()); if self.language_server_ids.contains_key(&key) { @@ -2747,23 +2755,22 @@ impl Project { buffers: impl IntoIterator>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers + let language_server_lookup_info: HashSet<(ModelHandle, Arc)> = buffers .into_iter() .filter_map(|buffer| { let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx).as_local()?; let full_path = file.full_path(cx); let language = self .languages .language_for_file(&full_path, Some(buffer.as_rope())) .now_or_never()? .ok()?; - Some((worktree.id(), worktree.abs_path().clone(), language)) + Some((file.worktree.clone(), language)) }) .collect(); - for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { - self.restart_language_servers(worktree_id, worktree_abs_path, language, cx); + for (worktree, language) in language_server_lookup_info { + self.restart_language_servers(worktree, language, cx); } None @@ -2772,11 +2779,13 @@ impl Project { // TODO This will break in the case where the adapter's root paths and worktrees are not equal fn restart_language_servers( &mut self, - worktree_id: WorktreeId, - fallback_path: Arc, + worktree: ModelHandle, language: Arc, cx: &mut ModelContext, ) { + let worktree_id = worktree.read(cx).id(); + let fallback_path = worktree.read(cx).abs_path(); + let mut stops = Vec::new(); for adapter in language.lsp_adapters() { stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); @@ -2806,7 +2815,7 @@ impl Project { .map(|path_buf| Arc::from(path_buf.as_path())) .unwrap_or(fallback_path); - this.start_language_servers(worktree_id, root_path, language.clone(), cx); + this.start_language_servers(&worktree, root_path, language.clone(), cx); // Lookup new server ids and set them for each of the orphaned worktrees for adapter in language.lsp_adapters() { @@ -3430,7 +3439,12 @@ impl Project { for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { let settings = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).clone() + language_settings( + language_name.as_deref(), + buffer.file().map(|f| f.as_ref()), + cx, + ) + .clone() }); let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -4439,11 +4453,15 @@ impl Project { push_to_history: bool, cx: &mut ModelContext, ) -> Task>> { - let tab_size = buffer.read_with(cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), cx).tab_size + let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { + let position = position.to_point_utf16(buffer); + let language_name = buffer.language_at(position).map(|l| l.name()); + let file = buffer.file().map(|f| f.as_ref()); + ( + position, + language_settings(language_name.as_deref(), file, cx).tab_size, + ) }); - let position = position.to_point_utf16(buffer.read(cx)); self.request_lsp( buffer.clone(), OnTypeFormatting { @@ -4849,6 +4867,7 @@ impl Project { worktree::Event::UpdatedEntries(changes) => { this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx); + this.update_local_worktree_settings(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) @@ -5155,6 +5174,61 @@ impl Project { .detach(); } + pub fn update_local_worktree_settings( + &mut self, + worktree: &ModelHandle, + changes: &UpdatedEntriesSet, + cx: &mut ModelContext, + ) { + let worktree_id = worktree.id(); + let worktree = worktree.read(cx).as_local().unwrap(); + + let mut settings_contents = Vec::new(); + for (path, _, change) in changes.iter() { + if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let settings_dir = Arc::from( + path.ancestors() + .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) + .unwrap(), + ); + let fs = self.fs.clone(); + let removed = *change == PathChange::Removed; + let abs_path = worktree.absolutize(path); + settings_contents.push(async move { + anyhow::Ok(( + settings_dir, + (!removed).then_some(fs.load(&abs_path).await?), + )) + }); + } + } + + if settings_contents.is_empty() { + return; + } + + cx.spawn_weak(move |_, mut cx| async move { + let settings_contents = futures::future::join_all(settings_contents).await; + cx.update(|cx| { + cx.update_global::(|store, cx| { + for entry in settings_contents { + if let Some((directory, file_content)) = entry.log_err() { + store + .set_local_settings( + worktree_id, + directory, + file_content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + } + } + }); + }); + }) + .detach(); + } + pub fn set_active_path(&mut self, entry: Option, cx: &mut ModelContext) { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 34b63fd5bd9fcaff67f9736ac79f6302045fd8f0..577b22d7309e3afeba8ae1f5a5ccb376c5aeeffb 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -63,6 +63,62 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_managing_project_specific_settings( + deterministic: Arc, + cx: &mut gpui::TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/the-root", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a": { + "a.rs": "fn a() {\n A\n}" + }, + "b": { + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "b.rs": "fn b() {\n B\n}" + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap()); + + deterministic.run_until_parked(); + cx.read(|cx| { + let tree = worktree.read(cx); + + let settings_a = language_settings( + None, + Some(&File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + )), + cx, + ); + let settings_b = language_settings( + None, + Some(&File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + )), + cx, + ); + + assert_eq!(settings_a.tab_size.get(), 8); + assert_eq!(settings_b.tab_size.get(), 2); + }); +} + #[gpui::test] async fn test_managing_language_servers( deterministic: Arc, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index dc3c172775721c5142aedab6fc4f0f5d66a1c913..7432eb8d40b5ce7da02b3fb06c007aaa74de1730 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -677,6 +677,18 @@ impl Worktree { Worktree::Remote(worktree) => worktree.abs_path.clone(), } } + + pub fn root_file(&self, cx: &mut ModelContext) -> Option { + let entry = self.entry_for_path("")?; + Some(File { + worktree: cx.handle(), + path: entry.path.clone(), + mtime: entry.mtime, + entry_id: entry.id, + is_local: self.is_local(), + is_deleted: false, + }) + } } impl LocalWorktree { @@ -684,14 +696,6 @@ impl LocalWorktree { path.starts_with(&self.abs_path) } - fn absolutize(&self, path: &Path) -> PathBuf { - if path.file_name().is_some() { - self.abs_path.join(path) - } else { - self.abs_path.to_path_buf() - } - } - pub(crate) fn load_buffer( &mut self, id: u64, @@ -1544,6 +1548,14 @@ impl Snapshot { &self.abs_path } + pub fn absolutize(&self, path: &Path) -> PathBuf { + if path.file_name().is_some() { + self.abs_path.join(path) + } else { + self.abs_path.to_path_buf() + } + } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { self.entries_by_id.get(&entry_id, &()).is_some() } @@ -2383,6 +2395,10 @@ impl language::File for File { .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) } + fn worktree_id(&self) -> usize { + self.worktree.id() + } + fn is_deleted(&self) -> bool { self.is_deleted } @@ -2447,6 +2463,17 @@ impl language::LocalFile for File { } impl File { + pub fn for_entry(entry: Entry, worktree: ModelHandle) -> Self { + Self { + worktree, + path: entry.path.clone(), + mtime: entry.mtime, + entry_id: entry.id, + is_local: true, + is_deleted: false, + } + } + pub fn from_proto( proto: rpc::proto::File, worktree: ModelHandle, @@ -2507,7 +2534,7 @@ pub enum EntryKind { File(CharBag), } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum PathChange { /// A filesystem entry was was created. Added, diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index cca2909da22dba93842304b87c600e2b05a99ae4..4c98dca51a4e661126843b28615e1cdb8a53accc 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -4,7 +4,14 @@ use assets::Assets; use fs::Fs; use futures::{channel::mpsc, StreamExt}; use gpui::{executor::Background, AppContext, AssetSource}; -use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; +use std::{ + borrow::Cow, + io::ErrorKind, + path::{Path, PathBuf}, + str, + sync::Arc, + time::Duration, +}; use util::{paths, ResultExt}; pub fn register(cx: &mut AppContext) { @@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T { cx.global::().get(None) } +pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T { + cx.global::().get(location) +} + pub fn default_settings() -> Cow<'static, str> { match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 71b3cc635f4e03465d94cb498567c21bd36bd76a..2560d0b752da72881b8ba373772159e07541c74b 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -89,14 +89,14 @@ pub struct SettingsStore { setting_values: HashMap>, default_deserialized_settings: Option, user_deserialized_settings: Option, - local_deserialized_settings: BTreeMap, serde_json::Value>, + local_deserialized_settings: BTreeMap<(usize, Arc), serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } #[derive(Debug)] struct SettingValue { global_value: Option, - local_values: Vec<(Arc, T)>, + local_values: Vec<(usize, Arc, T)>, } trait AnySettingValue { @@ -109,9 +109,9 @@ trait AnySettingValue { custom: &[DeserializedSetting], cx: &AppContext, ) -> Result>; - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any; fn set_global_value(&mut self, value: Box); - fn set_local_value(&mut self, path: Arc, value: Box); + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box); fn json_schema( &self, generator: &mut SchemaGenerator, @@ -165,7 +165,7 @@ impl SettingsStore { /// /// Panics if the given setting type has not been registered, or if there is no /// value for this setting. - pub fn get(&self, path: Option<&Path>) -> &T { + pub fn get(&self, path: Option<(usize, &Path)>) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) @@ -343,17 +343,19 @@ impl SettingsStore { /// Add or remove a set of local settings via a JSON string. pub fn set_local_settings( &mut self, + root_id: usize, path: Arc, settings_content: Option<&str>, cx: &AppContext, ) -> Result<()> { if let Some(content) = settings_content { self.local_deserialized_settings - .insert(path.clone(), parse_json_with_comments(content)?); + .insert((root_id, path.clone()), parse_json_with_comments(content)?); } else { - self.local_deserialized_settings.remove(&path); + self.local_deserialized_settings + .remove(&(root_id, path.clone())); } - self.recompute_values(Some(&path), cx)?; + self.recompute_values(Some((root_id, &path)), cx)?; Ok(()) } @@ -436,12 +438,12 @@ impl SettingsStore { fn recompute_values( &mut self, - changed_local_path: Option<&Path>, + changed_local_path: Option<(usize, &Path)>, cx: &AppContext, ) -> Result<()> { // Reload the global and local values for every setting. let mut user_settings_stack = Vec::::new(); - let mut paths_stack = Vec::>::new(); + let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { if let Some(default_settings) = &self.default_deserialized_settings { let default_settings = setting_value.deserialize_setting(default_settings)?; @@ -469,11 +471,11 @@ impl SettingsStore { } // Reload the local values for the setting. - for (path, local_settings) in &self.local_deserialized_settings { + for ((root_id, path), local_settings) in &self.local_deserialized_settings { // Build a stack of all of the local values for that setting. - while let Some(prev_path) = paths_stack.last() { - if let Some(prev_path) = prev_path { - if !path.starts_with(prev_path) { + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !path.starts_with(prev_path) { paths_stack.pop(); user_settings_stack.pop(); continue; @@ -485,14 +487,17 @@ impl SettingsStore { if let Some(local_settings) = setting_value.deserialize_setting(&local_settings).log_err() { - paths_stack.push(Some(path.as_ref())); + paths_stack.push(Some((*root_id, path.as_ref()))); user_settings_stack.push(local_settings); // If a local settings file changed, then avoid recomputing local // settings for any path outside of that directory. - if changed_local_path.map_or(false, |changed_local_path| { - !path.starts_with(changed_local_path) - }) { + if changed_local_path.map_or( + false, + |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !path.starts_with(changed_local_path) + }, + ) { continue; } @@ -500,7 +505,7 @@ impl SettingsStore { .load_setting(&default_settings, &user_settings_stack, cx) .log_err() { - setting_value.set_local_value(path.clone(), value); + setting_value.set_local_value(*root_id, path.clone(), value); } } } @@ -510,6 +515,24 @@ impl SettingsStore { } } +impl Debug for SettingsStore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SettingsStore") + .field( + "types", + &self + .setting_values + .values() + .map(|value| value.setting_type_name()) + .collect::>(), + ) + .field("default_settings", &self.default_deserialized_settings) + .field("user_settings", &self.user_deserialized_settings) + .field("local_settings", &self.local_deserialized_settings) + .finish_non_exhaustive() + } +} + impl AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { T::KEY @@ -546,10 +569,10 @@ impl AnySettingValue for SettingValue { Ok(DeserializedSetting(Box::new(value))) } - fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { - if let Some(path) = path { - for (settings_path, value) in self.local_values.iter().rev() { - if path.starts_with(&settings_path) { + fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any { + if let Some((root_id, path)) = path { + for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { + if root_id == *settings_root_id && path.starts_with(&settings_path) { return value; } } @@ -563,11 +586,14 @@ impl AnySettingValue for SettingValue { self.global_value = Some(*value.downcast().unwrap()); } - fn set_local_value(&mut self, path: Arc, value: Box) { + fn set_local_value(&mut self, root_id: usize, path: Arc, value: Box) { let value = *value.downcast().unwrap(); - match self.local_values.binary_search_by_key(&&path, |e| &e.0) { - Ok(ix) => self.local_values[ix].1 = value, - Err(ix) => self.local_values.insert(ix, (path, value)), + match self + .local_values + .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) + { + Ok(ix) => self.local_values[ix].2 = value, + Err(ix) => self.local_values.insert(ix, (root_id, path, value)), } } @@ -884,6 +910,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root1").into(), Some(r#"{ "user": { "staff": true } }"#), cx, @@ -891,6 +918,7 @@ mod tests { .unwrap(); store .set_local_settings( + 1, Path::new("/root1/subdir").into(), Some(r#"{ "user": { "name": "Jane Doe" } }"#), cx, @@ -899,6 +927,7 @@ mod tests { store .set_local_settings( + 1, Path::new("/root2").into(), Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), cx, @@ -906,7 +935,7 @@ mod tests { .unwrap(); assert_eq!( - store.get::(Some(Path::new("/root1/something"))), + store.get::(Some((1, Path::new("/root1/something")))), &UserSettings { name: "John Doe".to_string(), age: 31, @@ -914,7 +943,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root1/subdir/something"))), + store.get::(Some((1, Path::new("/root1/subdir/something")))), &UserSettings { name: "Jane Doe".to_string(), age: 31, @@ -922,7 +951,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &UserSettings { name: "John Doe".to_string(), age: 42, @@ -930,7 +959,7 @@ mod tests { } ); assert_eq!( - store.get::(Some(Path::new("/root2/something"))), + store.get::(Some((1, Path::new("/root2/something")))), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 767e3bf4dbb3847064c7a32d217596840457c080..2842dfa8a4f2592830ce3d092e2bfec4ebd2e35c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -905,7 +905,10 @@ mod tests { cx: &mut TestAppContext, ) -> (ModelHandle, ViewHandle) { let params = cx.update(AppState::test); - cx.update(|cx| theme::init((), cx)); + cx.update(|cx| { + theme::init((), cx); + language::init(cx); + }); let project = Project::test(params.fs.clone(), [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f998fc319fcb7bcdfc6be70c66b482ec247a93b0..e3397a1557cfeed520e4c56fa38c40e4539d71a9 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,6 +15,7 @@ lazy_static::lazy_static! { pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); + pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); } pub mod legacy { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index bd5f2b4c021e7d8239d76ea29d8ef88ddcf8015b..d66602ee04cb58da09d8b037020cb1fb483937fa 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -107,7 +107,7 @@ impl LspAdapter for YamlLspAdapter { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": language_settings(Some("YAML"), cx).tab_size, + "editor.tabSize": language_settings(Some("YAML"), None, cx).tab_size, } })) .boxed(), From ed0fa2404c3cc53fb4e59a21692fcc18bfe1f4b4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 29 May 2023 14:31:54 -0700 Subject: [PATCH 009/101] Use settings JSON schema when editing local settings files --- crates/zed/src/languages/json.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 406d54cc03907e94d2b76c4e87e97941458ac694..3947c62a6d96d11ba37444b34930f75bb8fbaec1 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -135,7 +135,10 @@ impl LspAdapter for JsonLspAdapter { }, "schemas": [ { - "fileMatch": [schema_file_match(&paths::SETTINGS)], + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], "schema": settings_schema, }, { From 69e8a166e4e3409583678d880f429c8c72e51b18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 30 May 2023 15:25:53 +0200 Subject: [PATCH 010/101] Start on `assistant::QuoteSelection` --- assets/keymaps/default.json | 3 +- crates/ai/src/assistant.rs | 85 ++++++++++++++++++++++++++++++- crates/workspace/src/dock.rs | 6 +++ crates/workspace/src/workspace.rs | 10 ++++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 0b89969c70806752dcfb1b15ba48f6f4fe91868b..c6964b49e27dbff4d369656d88cd0a000abc5c13 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -199,7 +199,8 @@ "context": "ContextEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", - "escape": "assistant::CancelLastAssist" + "escape": "assistant::CancelLastAssist", + "cmd-?": "assistant::QuoteSelection" } }, { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 23baaef04ac8470e0011e2ab7c47ffc0ab86484a..6e76e46c6102cb22de172e0a132d58018883e39c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,11 +19,15 @@ use workspace::{ pane, Pane, Workspace, }; -actions!(assistant, [NewContext, Assist, CancelLastAssist]); +actions!( + assistant, + [NewContext, Assist, CancelLastAssist, QuoteSelection] +); pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); + cx.add_action(AssistantEditor::quote_selection); } pub enum AssistantPanelEvent { @@ -136,6 +140,12 @@ impl View for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { ChildView::new(&self.pane, cx).into_any() } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.pane); + } + } } impl Panel for AssistantPanel { @@ -361,7 +371,7 @@ impl AssistantEditor { editor.set_render_excerpt_header( { let assistant = assistant.clone(); - move |editor, params: editor::RenderExcerptHeaderParams, cx| { + move |_editor, params: editor::RenderExcerptHeaderParams, cx| { let style = &theme::current(cx).assistant; if let Some(message) = assistant.read(cx).messages_by_id.get(¶ms.id) { let sender = match message.role { @@ -421,6 +431,71 @@ impl AssistantEditor { cx.propagate_action(); } } + + fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::()) else { + return; + }; + + let text = editor.read_with(cx, |editor, cx| { + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + + let selected_text = buffer.text_for_range(range).collect::(); + if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + format!("```{language_name}\n{selected_text}\n```") + }) + } + }); + + // Activate the panel + if !panel.read(cx).has_focus(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(text) = text { + panel.update(cx, |panel, cx| { + if let Some(assistant) = panel + .pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .ok_or_else(|| anyhow!("no active context")) + .log_err() + { + assistant.update(cx, |assistant, cx| { + assistant + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + } + }); + } + } } impl Entity for AssistantEditor { @@ -440,6 +515,12 @@ impl View for AssistantEditor { .with_style(theme.container) .into_any() } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } } impl Item for AssistantEditor { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 73d4f79399bd73bd52c9b0ecce04076e76b0407e..49651486db3b12526d9e40cc92d01a1a13c3ca19 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -184,6 +184,12 @@ impl Dock { .map_or(false, |panel| panel.has_focus(cx)) } + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.as_any().clone().downcast()) + } + pub fn panel_index_for_type(&self) -> Option { self.panel_entries .iter() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8afad46ea108c0ab5ef1e6eac4ccf19e1f4661f1..2aa937c9a932ddbc2e2cb9364607693cb2ab5621 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1678,6 +1678,16 @@ impl Workspace { } } + pub fn panel(&self, cx: &WindowContext) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + let dock = dock.read(cx); + if let Some(panel) = dock.panel::() { + return Some(panel); + } + } + None + } + fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); From c9820fde61acd5673dda49c3e4144b8e6bfd1b3b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 30 May 2023 17:48:41 -0700 Subject: [PATCH 011/101] WIP: Add toast when users attempt to use shift-escape for the first time --- crates/util/src/channel.rs | 2 + crates/workspace/src/notifications.rs | 166 ++++++++++++++------------ crates/workspace/src/pane.rs | 10 +- crates/workspace/src/workspace.rs | 60 +++++++++- crates/zed/src/main.rs | 1 + 5 files changed, 157 insertions(+), 82 deletions(-) diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 274fd576a050076511c8c1253b7187fbd437e8c3..2b45cb6b6611cb6a036f1cf61768fcd2f5dac184 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -2,6 +2,8 @@ use std::env; use lazy_static::lazy_static; +pub struct ZedVersion(pub &'static str); + lazy_static! { pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) { env::var("ZED_RELEASE_CHANNEL") diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 21b3be09d06ea6781e70dd12fa91a1c2ace2e152..4437a960ef250e1a025c3fb1255826823cd42fda 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -1,5 +1,5 @@ use crate::{Toast, Workspace}; -use collections::HashSet; +use collections::HashMap; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use std::{any::TypeId, ops::DerefMut}; @@ -34,11 +34,11 @@ impl From<&dyn NotificationHandle> for AnyViewHandle { } struct NotificationTracker { - notifications_sent: HashSet, + notifications_sent: HashMap>, } impl std::ops::Deref for NotificationTracker { - type Target = HashSet; + type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.notifications_sent @@ -54,24 +54,35 @@ impl DerefMut for NotificationTracker { impl NotificationTracker { fn new() -> Self { Self { - notifications_sent: HashSet::default(), + notifications_sent: Default::default(), } } } impl Workspace { + pub fn has_shown_notification_once( + &self, + id: usize, + cx: &ViewContext, + ) -> bool { + cx + .global::() + .get(&TypeId::of::()) + .map(|ids| ids.contains(&id)) + .unwrap_or(false) + } + pub fn show_notification_once( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { - if !cx - .global::() - .contains(&TypeId::of::()) + if !self.has_shown_notification_once::(id, cx) { cx.update_global::(|tracker, _| { - tracker.insert(TypeId::of::()) + let entry = tracker.entry(TypeId::of::()).or_default(); + entry.push(id); }); self.show_notification::(id, cx, build_notification) @@ -247,80 +258,81 @@ pub mod simple_message_notification { let on_click = self.on_click.clone(); let has_click_action = on_click.is_some(); - MouseEventHandler::::new(0, cx, |state, cx| { - Flex::column() - .with_child( - Flex::row() - .with_child( - Text::new(message, theme.message.text.clone()) - .contained() - .with_style(theme.message.container) - .aligned() - .top() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_padding(Padding::uniform(5.)) - .on_click(MouseButton::Left, move |_, this, cx| { - this.dismiss(&Default::default(), cx); - }) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .constrained() - .with_height( - cx.font_cache().line_height(theme.message.text.font_size), - ) + Flex::column() + .with_child( + Flex::row() + .with_child( + Text::new(message, theme.message.text.clone()) + .contained() + .with_style(theme.message.container) .aligned() .top() - .flex_float(), - ), - ) - .with_children({ - let style = theme.action_message.style_for(state, false); - if let Some(click_message) = click_message { - Some( - Flex::row().with_child( - Text::new(click_message, style.text.clone()) + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/x_mark_8.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_padding(Padding::uniform(5.)) + .on_click(MouseButton::Left, move |_, this, cx| { + this.dismiss(&Default::default(), cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .constrained() + .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + .aligned() + .top() + .flex_float(), + ), + ) + .with_children({ + click_message + .map(|click_message| { + MouseEventHandler::::new( + 0, + cx, + |state, _| { + let style = theme.action_message.style_for(state, false); + + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container), + ) .contained() - .with_style(style.container), - ), + }, ) - } else { - None - } + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(on_click) = on_click.as_ref() { + on_click(cx); + this.dismiss(&Default::default(), cx); + } + }) + // Since we're not using a proper overlay, we have to capture these extra events + .on_down(MouseButton::Left, |_, _, _| {}) + .on_up(MouseButton::Left, |_, _, _| {}) + .with_cursor_style(if has_click_action { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + }) .into_iter() - }) - .contained() - }) - // Since we're not using a proper overlay, we have to capture these extra events - .on_down(MouseButton::Left, |_, _, _| {}) - .on_up(MouseButton::Left, |_, _, _| {}) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(on_click) = on_click.as_ref() { - on_click(cx); - this.dismiss(&Default::default(), cx); - } - }) - .with_cursor_style(if has_click_action { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .into_any() + }) + .into_any() } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 921ae5e0100d45ced85f1664dfe9a1acf534892a..fad8cd8864e1a55cec7d214201abb35eb60f9404 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,8 +2,8 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; use crate::{ - item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile, - NewSearch, ToggleZoom, Workspace, WorkspaceSettings, + item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item, + NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; @@ -536,6 +536,12 @@ impl Pane { } pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // Potentially warn the user of the new keybinding + let workspace_handle = self.workspace().clone(); + cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) + .detach(); + + if self.zoomed { cx.emit(Event::ZoomOut); } else if !self.items.is_empty() { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d051f6d80a9f6a2ee8974797333ac3ae0d6ddfe8..292ec28abc664335caa97d0402d62e3fbc6e5215 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,7 @@ use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, UserStore, ZED_APP_VERSION, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -83,7 +83,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::Theme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, paths, ResultExt}; +use util::{async_iife, channel::ZedVersion, paths, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -3190,6 +3190,60 @@ async fn open_items( opened_items } +fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { + const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; + const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; + + if workspace + .read_with(cx, |workspace, cx| { + let version = cx.global::().0; + if !version.contains("0.88") + && !version.contains("0.89") + && !version.contains("0.90") + && !version.contains("0.91") + && !version.contains("0.92") + { + return true; + } + workspace.has_shown_notification_once::(2, cx) + }) + .unwrap_or(false) + { + return; + } + + if db::kvp::KEY_VALUE_STORE + .read_kvp(NEW_DOCK_HINT_KEY) + .ok() + .flatten() + .is_some() + { + return; + } + + cx.spawn(|_| async move { + db::kvp::KEY_VALUE_STORE + .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string()) + .await + .ok(); + }) + .detach(); + + workspace + .update(cx, |workspace, cx| { + workspace.show_notification_once(2, cx, |cx| { + cx.add_view(|_| { + MessageNotification::new( + "Looking for the dock? Try 'ctrl-`'!\n'shift-escape' now zooms your pane", + ) + .with_click_message("Click to read more about the new panel system") + .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) + }) + }) + }) + .ok(); +} + fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; @@ -3206,7 +3260,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut Asy } else { let backup_path = (*db::BACKUP_DB_PATH).read(); if let Some(backup_path) = backup_path.clone() { - workspace.show_notification_once(0, cx, move |cx| { + workspace.show_notification_once(1, cx, move |cx| { cx.add_view(move |_| { MessageNotification::new(format!( "Database file was corrupted. Old database backed up to {}", diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31f331ef93ef17eecb4870f3ed23c9f963a5b3aa..558796807bedc4f8151faaa8cf0c6544ac5f5423 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -119,6 +119,7 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(util::channel::ZedVersion(env!("CARGO_PKG_VERSION"))); #[cfg(debug_assertions)] cx.set_global(StaffMode(true)); From 8f95435548ee3ef34085992588e4f4107bf1b346 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 30 May 2023 11:05:08 -0700 Subject: [PATCH 012/101] Replicate project-specific settings when collaborating --- .../20221109000000_test_schema.sql | 10 ++ ...0529164700_add_worktree_settings_files.sql | 10 ++ crates/collab/src/db.rs | 101 ++++++++++++++ .../collab/src/db/worktree_settings_file.rs | 19 +++ crates/collab/src/rpc.rs | 50 +++++++ crates/collab/src/tests/integration_tests.rs | 129 ++++++++++++++++++ crates/project/src/project.rs | 105 +++++++++++--- crates/rpc/proto/zed.proto | 9 ++ crates/rpc/src/proto.rs | 2 + crates/settings/src/settings_store.rs | 15 ++ 10 files changed, 432 insertions(+), 18 deletions(-) create mode 100644 crates/collab/migrations/20230529164700_add_worktree_settings_files.sql create mode 100644 crates/collab/src/db/worktree_settings_file.rs diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7c6a49f179c2258cc2f18a834e8d63c9c70a6df8..b0b2a684d9501bf135f4280022437ef1c39f1230 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -112,6 +112,16 @@ CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_rep CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id"); CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id"); +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INTEGER NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id"); CREATE TABLE "worktree_diagnostic_summaries" ( "project_id" INTEGER NOT NULL, diff --git a/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql new file mode 100644 index 0000000000000000000000000000000000000000..973a40af0f21908e5dbe0d5a30373629f24b7f1e --- /dev/null +++ b/crates/collab/migrations/20230529164700_add_worktree_settings_files.sql @@ -0,0 +1,10 @@ +CREATE TABLE "worktree_settings_files" ( + "project_id" INTEGER NOT NULL, + "worktree_id" INT8 NOT NULL, + "path" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + PRIMARY KEY(project_id, worktree_id, path), + FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE +); +CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id"); +CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id"); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index fd28fb910177099d0cf7d639cfd802aa4f3dab25..1deca1baa8036113c4ad2c4f0535a18085cb5db1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -16,6 +16,7 @@ mod worktree_diagnostic_summary; mod worktree_entry; mod worktree_repository; mod worktree_repository_statuses; +mod worktree_settings_file; use crate::executor::Executor; use crate::{Error, Result}; @@ -1494,6 +1495,7 @@ impl Database { updated_repositories: Default::default(), removed_repositories: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }; @@ -1638,6 +1640,25 @@ impl Database { }) .collect::>(); + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees + .iter_mut() + .find(|w| w.id == db_settings_file.worktree_id as u64) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + let mut collaborators = project .find_related(project_collaborator::Entity) .all(&*tx) @@ -2637,6 +2658,58 @@ impl Database { .await } + pub async fn update_worktree_settings( + &self, + update: &proto::UpdateWorktreeSettings, + connection: ConnectionId, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + // Ensure the update comes from the host. + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection()? != connection { + return Err(anyhow!("can't update a project hosted by someone else"))?; + } + + if let Some(content) = &update.content { + worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + content: ActiveValue::Set(content.clone()), + }) + .on_conflict( + OnConflict::columns([ + worktree_settings_file::Column::ProjectId, + worktree_settings_file::Column::WorktreeId, + worktree_settings_file::Column::Path, + ]) + .update_column(worktree_settings_file::Column::Content) + .to_owned(), + ) + .exec(&*tx) + .await?; + } else { + worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel { + project_id: ActiveValue::Set(project_id), + worktree_id: ActiveValue::Set(update.worktree_id as i64), + path: ActiveValue::Set(update.path.clone()), + ..Default::default() + }) + .exec(&*tx) + .await?; + } + + let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?; + Ok(connection_ids) + }) + .await + } + pub async fn join_project( &self, project_id: ProjectId, @@ -2707,6 +2780,7 @@ impl Database { entries: Default::default(), repository_entries: Default::default(), diagnostic_summaries: Default::default(), + settings_files: Default::default(), scan_id: db_worktree.scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64, }, @@ -2819,6 +2893,25 @@ impl Database { } } + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = + worktrees.get_mut(&(db_settings_file.worktree_id as u64)) + { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + // Populate language servers. let language_servers = project .find_related(language_server::Entity) @@ -3482,6 +3575,7 @@ pub struct RejoinedWorktree { pub updated_repositories: Vec, pub removed_repositories: Vec, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } @@ -3537,10 +3631,17 @@ pub struct Worktree { pub entries: Vec, pub repository_entries: BTreeMap, pub diagnostic_summaries: Vec, + pub settings_files: Vec, pub scan_id: u64, pub completed_scan_id: u64, } +#[derive(Debug)] +pub struct WorktreeSettingsFile { + pub path: String, + pub content: String, +} + #[cfg(test)] pub use test::*; diff --git a/crates/collab/src/db/worktree_settings_file.rs b/crates/collab/src/db/worktree_settings_file.rs new file mode 100644 index 0000000000000000000000000000000000000000..f8e87f6e599cbaaeb698bdb28cb1316c7097dc77 --- /dev/null +++ b/crates/collab/src/db/worktree_settings_file.rs @@ -0,0 +1,19 @@ +use super::ProjectId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "worktree_settings_files")] +pub struct Model { + #[sea_orm(primary_key)] + pub project_id: ProjectId, + #[sea_orm(primary_key)] + pub worktree_id: i64, + #[sea_orm(primary_key)] + pub path: String, + pub content: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4c117b613da90ce6ac54c792833aa94364715758..61c6123a827e897d9bf8c5db15e9e79fdbd4b951 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -200,6 +200,7 @@ impl Server { .add_message_handler(start_language_server) .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) + .add_message_handler(update_worktree_settings) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) @@ -1088,6 +1089,18 @@ async fn rejoin_room( }, )?; } + + for settings_file in worktree.settings_files { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project.id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1410,6 +1423,18 @@ async fn join_project( }, )?; } + + for settings_file in dbg!(worktree.settings_files) { + session.peer.send( + session.connection_id, + proto::UpdateWorktreeSettings { + project_id: project_id.to_proto(), + worktree_id: worktree.id, + path: settings_file.path, + content: Some(settings_file.content), + }, + )?; + } } for language_server in &project.language_servers { @@ -1525,6 +1550,31 @@ async fn update_diagnostic_summary( Ok(()) } +async fn update_worktree_settings( + message: proto::UpdateWorktreeSettings, + session: Session, +) -> Result<()> { + dbg!(&message); + + let guest_connection_ids = session + .db() + .await + .update_worktree_settings(&message, session.connection_id) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, message.clone()) + }, + ); + + Ok(()) +} + async fn start_language_server( request: proto::StartLanguageServer, session: Session, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d771f969d8899e0183d95db17a42389dbe706dd0..fb0f63b2438b584739cef83aafbf4f9782da89dc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3114,6 +3114,135 @@ async fn test_fs_operations( }); } +#[gpui::test(iterations = 10)] +async fn test_local_settings( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + // As client A, open a project that contains some local settings files + client_a + .fs + .insert_tree( + "/dir", + json!({ + ".zed": { + "settings.json": r#"{ "tab_size": 2 }"# + }, + "a": { + ".zed": { + "settings.json": r#"{ "tab_size": 8 }"# + }, + "a.txt": "a-contents", + }, + "b": { + "b.txt": "b-contents", + } + }), + ) + .await; + let (project_a, _) = client_a.build_local_project("/dir", cx_a).await; + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // As client B, join that project and observe the local settings. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap()); + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{"tab_size":2}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, update a settings file. As Client B, see the changed settings. + client_a + .fs + .insert_file("/dir/.zed/settings.json", r#"{}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("").into(), r#"{}"#.to_string()), + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + ] + ) + }); + + // As client A, create and remove some settings files. As client B, see the changed settings. + client_a + .fs + .remove_file("/dir/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + client_a + .fs + .create_dir("/dir/b/.zed".as_ref()) + .await + .unwrap(); + client_a + .fs + .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into()) + .await; + deterministic.run_until_parked(); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[ + (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()), + (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()), + ] + ) + }); + + // As client B, disconnect. + server.forbid_connections(); + server.disconnect_client(client_b.peer_id().unwrap()); + + // As client A, change and remove settings files while client B is disconnected. + client_a + .fs + .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into()) + .await; + client_a + .fs + .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default()) + .await + .unwrap(); + deterministic.run_until_parked(); + + // As client B, reconnect and see the changed settings. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + cx_b.read(|cx| { + let store = cx.global::(); + assert_eq!( + store.local_settings(worktree_b.id()).collect::>(), + &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),] + ) + }); +} + #[gpui::test(iterations = 10)] async fn test_buffer_conflict_after_save( deterministic: Arc, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c76587ff56328fabdab93abf77965e547f11979b..3cc4a181c55ad18315204c780aa6c6a6da337666 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -462,6 +462,7 @@ impl Project { client.add_model_request_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_update_worktree_settings); client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); @@ -1105,6 +1106,21 @@ impl Project { .log_err(); } + let store = cx.global::(); + for worktree in self.worktrees(cx) { + let worktree_id = worktree.read(cx).id().to_proto(); + for (path, content) in store.local_settings(worktree.id()) { + self.client + .send(proto::UpdateWorktreeSettings { + project_id, + worktree_id, + path: path.to_string_lossy().into(), + content: Some(content), + }) + .log_err(); + } + } + let (updates_tx, mut updates_rx) = mpsc::unbounded(); let client = self.client.clone(); self.client_state = Some(ProjectClientState::Local { @@ -1217,6 +1233,14 @@ impl Project { message_id: u32, cx: &mut ModelContext, ) -> Result<()> { + cx.update_global::(|store, cx| { + for worktree in &self.worktrees { + store + .clear_local_settings(worktree.handle_id(), cx) + .log_err(); + } + }); + self.join_project_response_message_id = message_id; self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?; @@ -4888,8 +4912,12 @@ impl Project { .push(WorktreeHandle::Weak(worktree.downgrade())); } - cx.observe_release(worktree, |this, worktree, cx| { + let handle_id = worktree.id(); + cx.observe_release(worktree, move |this, worktree, cx| { let _ = this.remove_worktree(worktree.id(), cx); + cx.update_global::(|store, cx| { + store.clear_local_settings(handle_id, cx).log_err() + }); }) .detach(); @@ -5174,14 +5202,16 @@ impl Project { .detach(); } - pub fn update_local_worktree_settings( + fn update_local_worktree_settings( &mut self, worktree: &ModelHandle, changes: &UpdatedEntriesSet, cx: &mut ModelContext, ) { + let project_id = self.remote_id(); let worktree_id = worktree.id(); let worktree = worktree.read(cx).as_local().unwrap(); + let remote_worktree_id = worktree.id(); let mut settings_contents = Vec::new(); for (path, _, change) in changes.iter() { @@ -5195,10 +5225,7 @@ impl Project { let removed = *change == PathChange::Removed; let abs_path = worktree.absolutize(path); settings_contents.push(async move { - anyhow::Ok(( - settings_dir, - (!removed).then_some(fs.load(&abs_path).await?), - )) + (settings_dir, (!removed).then_some(fs.load(&abs_path).await)) }); } } @@ -5207,19 +5234,30 @@ impl Project { return; } + let client = self.client.clone(); cx.spawn_weak(move |_, mut cx| async move { - let settings_contents = futures::future::join_all(settings_contents).await; + let settings_contents: Vec<(Arc, _)> = + futures::future::join_all(settings_contents).await; cx.update(|cx| { cx.update_global::(|store, cx| { - for entry in settings_contents { - if let Some((directory, file_content)) = entry.log_err() { - store - .set_local_settings( - worktree_id, - directory, - file_content.as_ref().map(String::as_str), - cx, - ) + for (directory, file_content) in settings_contents { + let file_content = file_content.and_then(|content| content.log_err()); + store + .set_local_settings( + worktree_id, + directory.clone(), + file_content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + if let Some(remote_id) = project_id { + client + .send(proto::UpdateWorktreeSettings { + project_id: remote_id, + worktree_id: remote_worktree_id.to_proto(), + path: directory.to_string_lossy().into_owned(), + content: file_content, + }) .log_err(); } } @@ -5467,6 +5505,30 @@ impl Project { }) } + async fn handle_update_worktree_settings( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + if let Some(worktree) = this.worktree_for_id(worktree_id, cx) { + cx.update_global::(|store, cx| { + store + .set_local_settings( + worktree.id(), + PathBuf::from(&envelope.payload.path).into(), + envelope.payload.content.as_ref().map(String::as_str), + cx, + ) + .log_err(); + }); + } + Ok(()) + }) + } + async fn handle_create_project_entry( this: ModelHandle, envelope: TypedEnvelope, @@ -6557,8 +6619,8 @@ impl Project { } self.metadata_changed(cx); - for (id, _) in old_worktrees_by_id { - cx.emit(Event::WorktreeRemoved(id)); + for id in old_worktrees_by_id.keys() { + cx.emit(Event::WorktreeRemoved(*id)); } Ok(()) @@ -6928,6 +6990,13 @@ impl WorktreeHandle { WorktreeHandle::Weak(handle) => handle.upgrade(cx), } } + + pub fn handle_id(&self) -> usize { + match self { + WorktreeHandle::Strong(handle) => handle.id(), + WorktreeHandle::Weak(handle) => handle.id(), + } + } } impl OpenBuffer { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 848cc1c2fa3213ce55f317e63b7a6d7db293671f..8351cdfd2dfe6a61bcccaad4d7a8dc40ff00bfc2 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -132,6 +132,8 @@ message Envelope { OnTypeFormatting on_type_formatting = 111; OnTypeFormattingResponse on_type_formatting_response = 112; + + UpdateWorktreeSettings update_worktree_settings = 113; } } @@ -339,6 +341,13 @@ message UpdateWorktree { string abs_path = 10; } +message UpdateWorktreeSettings { + uint64 project_id = 1; + uint64 worktree_id = 2; + string path = 3; + optional string content = 4; +} + message CreateProjectEntry { uint64 project_id = 1; uint64 worktree_id = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 07925a0486e6d210ceac44a7c5a5428b3f48a58e..13794ea64dad1446e579dd00806ccb4afda4758b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -236,6 +236,7 @@ messages!( (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), (UpdateWorktree, Foreground), + (UpdateWorktreeSettings, Foreground), (UpdateDiffBase, Foreground), (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), @@ -345,6 +346,7 @@ entity_messages!( UpdateProject, UpdateProjectCollaborator, UpdateWorktree, + UpdateWorktreeSettings, UpdateDiffBase ); diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 2560d0b752da72881b8ba373772159e07541c74b..bb51fd1ed004911415abc292efc70c33f666685d 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -359,6 +359,21 @@ impl SettingsStore { Ok(()) } + /// Add or remove a set of local settings via a JSON string. + pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> { + eprintln!("clearing local settings {root_id}"); + self.local_deserialized_settings + .retain(|k, _| k.0 != root_id); + self.recompute_values(Some((root_id, "".as_ref())), cx)?; + Ok(()) + } + + pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator, String)> { + self.local_deserialized_settings + .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into())) + .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap())) + } + pub fn json_schema( &self, schema_params: &SettingsJsonSchemaParams, From eeba72d7755c6f1f2b4b1c8142b26212583deaec Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 09:03:45 -0700 Subject: [PATCH 013/101] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index b929de95961e756d369f9b1549dc693d6536bbe0..bef6efa529fd514b24789a5d294dbc33409974ab 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 56; +pub const PROTOCOL_VERSION: u32 = 57; From 76927b6d95bb8d7baeb38cdb39b59d365776e7c3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 10:29:59 -0700 Subject: [PATCH 014/101] Make active panel's button close its dock --- crates/workspace/src/dock.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 886afe943d0c6ff57c8c2f9fb5c385b8c09f7451..ac52c8f1790c865cdf3a1378b7a994dbe81a3500 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -472,11 +472,22 @@ impl View for PanelButtons { Flex::row() .with_children(panels.into_iter().enumerate().map( |(panel_ix, (view, context_menu))| { - let (tooltip, tooltip_action) = view.icon_tooltip(cx); + let is_active = is_open && panel_ix == active_ix; + let (tooltip, tooltip_action) = if is_active { + ( + format!("Close {} dock", dock_position.to_label()), + Some(match dock_position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + }), + ) + } else { + view.icon_tooltip(cx) + }; Stack::new() .with_child( MouseEventHandler::::new(panel_ix, cx, |state, cx| { - let is_active = is_open && panel_ix == active_ix; let style = button_style.style_for(state, is_active); Flex::row() .with_child( From ac41564a8f3ac0c575bc29acaf57ede2a7e6753d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 12:34:06 -0700 Subject: [PATCH 015/101] Dismiss zoomed panels by closing their dock, not zooming them out --- crates/workspace/src/dock.rs | 4 ++ crates/workspace/src/workspace.rs | 110 ++++++++++++++++-------------- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ac52c8f1790c865cdf3a1378b7a994dbe81a3500..a4b71fa5a73fa862f3b28cc4b3785d7c411c4583 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -175,6 +175,10 @@ impl Dock { } } + pub fn position(&self) -> DockPosition { + self.position + } + pub fn is_open(&self) -> bool { self.is_open } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d051f6d80a9f6a2ee8974797333ac3ae0d6ddfe8..d7219970535405a2c29d5bf1367877915d80f5ec 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -908,18 +908,24 @@ impl Workspace { } }); } else if T::should_zoom_in_on_event(event) { - this.zoom_out(cx); dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); if panel.has_focus(cx) { this.zoomed = Some(panel.downgrade().into_any()); this.zoomed_position = Some(panel.read(cx).position(cx)); } } else if T::should_zoom_out_on_event(event) { - this.zoom_out(cx); + dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); + if this.zoomed_position == Some(prev_position) { + this.zoomed = None; + this.zoomed_position = None; + } + cx.notify(); } else if T::is_focus_event(event) { + let position = panel.read(cx).position(cx); + this.dismiss_zoomed_items_to_reveal(Some(position), cx); if panel.is_zoomed(cx) { this.zoomed = Some(panel.downgrade().into_any()); - this.zoomed_position = Some(panel.read(cx).position(cx)); + this.zoomed_position = Some(position); } else { this.zoomed = None; this.zoomed_position = None; @@ -1592,7 +1598,7 @@ impl Workspace { DockPosition::Right => &self.right_dock, }; let mut focus_center = false; - let mut zoom_out = false; + let mut reveal_dock = false; dock.update(cx, |dock, cx| { let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); let was_visible = dock.is_open() && !other_is_zoomed; @@ -1607,14 +1613,15 @@ impl Workspace { if active_panel.is_zoomed(cx) { cx.focus(active_panel.as_any()); } - zoom_out = true; + reveal_dock = true; } } }); - if zoom_out { - self.zoom_out_everything_except(dock_side, cx); + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); } + if focus_center { cx.focus_self(); } @@ -1623,62 +1630,49 @@ impl Workspace { self.serialize_workspace(cx); } + /// Transfer focus to the panel of the given type. pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { - self.show_or_hide_panel::(cx, |_, _| true)? + self.focus_or_unfocus_panel::(cx, |_, _| true)? .as_any() .clone() .downcast() } + /// Focus the panel of the given type if it isn't already focused. If it is + /// already focused, then transfer focus back to the workspace center. pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { - self.show_or_hide_panel::(cx, |panel, cx| !panel.has_focus(cx)); + self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); } - fn show_or_hide_panel( + /// Focus or unfocus the given panel type, depending on the given callback. + fn focus_or_unfocus_panel( &mut self, cx: &mut ViewContext, - show: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, + should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, ) -> Option> { - for (dock, position) in [ - self.left_dock.clone(), - self.bottom_dock.clone(), - self.right_dock.clone(), - ] - .into_iter() - .zip( - [ - DockPosition::Left, - DockPosition::Bottom, - DockPosition::Right, - ] - .into_iter(), - ) { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { let mut focus_center = false; - let mut zoom_out = false; + let mut reveal_dock = false; let panel = dock.update(cx, |dock, cx| { dock.activate_panel(panel_index, cx); let panel = dock.active_panel().cloned(); if let Some(panel) = panel.as_ref() { - let should_show = show(&**panel, cx); - if should_show { + if should_focus(&**panel, cx) { dock.set_open(true, cx); cx.focus(panel.as_any()); - zoom_out = true; + reveal_dock = true; } else { - if panel.is_zoomed(cx) { - dock.set_open(false, cx); - } + // if panel.is_zoomed(cx) { + // dock.set_open(false, cx); + // } focus_center = true; } } panel }); - if zoom_out { - self.zoom_out_everything_except(position, cx); - } if focus_center { cx.focus_self(); } @@ -1705,28 +1699,38 @@ impl Workspace { cx.notify(); } - fn zoom_out_everything_except( + fn dismiss_zoomed_items_to_reveal( &mut self, - except_position: DockPosition, + except_position: Option, cx: &mut ViewContext, ) { - for pane in &self.panes { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - } - - if except_position != DockPosition::Left { - self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + // If a center pane is zoomed, unzoom it. + if except_position.is_some() { + for pane in &self.panes { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } } - if except_position != DockPosition::Bottom { - self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + // If another dock is zoomed, hide it. + let mut focus_center = false; + for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { + dock.update(cx, |dock, cx| { + if Some(dock.position()) != except_position { + if let Some(panel) = dock.active_panel() { + if panel.is_zoomed(cx) { + focus_center |= panel.has_focus(cx); + dock.set_open(false, cx); + } + } + } + }); } - if except_position != DockPosition::Right { - self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + if focus_center { + cx.focus_self(); } - if self.zoomed_position != Some(except_position) { + if self.zoomed_position != except_position { self.zoomed = None; self.zoomed_position = None; } @@ -1937,6 +1941,7 @@ impl Workspace { self.last_active_center_pane = Some(pane.downgrade()); } + self.dismiss_zoomed_items_to_reveal(None, cx); if pane.read(cx).is_zoomed() { self.zoomed = Some(pane.downgrade().into_any()); } else { @@ -1998,7 +2003,6 @@ impl Workspace { } pane::Event::ZoomIn => { if pane == self.active_pane { - self.zoom_out(cx); pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); if pane.read(cx).has_focus() { self.zoomed = Some(pane.downgrade().into_any()); @@ -2007,7 +2011,13 @@ impl Workspace { cx.notify(); } } - pane::Event::ZoomOut => self.zoom_out(cx), + pane::Event::ZoomOut => { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + if self.zoomed_position.is_none() { + self.zoomed = None; + } + cx.notify(); + } } self.serialize_workspace(cx); From a10933c063307fb768bd122a86f1ff359c0e93fe Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 31 May 2023 12:59:04 -0400 Subject: [PATCH 016/101] Update pane/panel zoom styling --- styles/src/styleTree/workspace.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index cf5234aa004eb29022a967f6bbdfcd135b0e5dbf..ec992c9c0b368aa7ee792aa3b2ce5c2da3045d2c 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -13,6 +13,7 @@ import tabBar from "./tabBar" export default function workspace(colorScheme: ColorScheme) { const layer = colorScheme.lowest + const isLight = colorScheme.isLight const itemSpacing = 8 const titlebarButton = { cornerRadius: 6, @@ -120,16 +121,18 @@ export default function workspace(colorScheme: ColorScheme) { }, zoomedBackground: { cursor: "Arrow", - background: withOpacity(background(colorScheme.lowest), 0.85) + background: isLight + ? withOpacity(background(colorScheme.lowest), 0.8) + : withOpacity(background(colorScheme.highest), 0.6) }, zoomedPaneForeground: { - margin: 10, + margin: 16, shadow: colorScheme.modalShadow, - border: border(colorScheme.highest, { overlay: true }), + border: border(colorScheme.lowest, { overlay: true }), }, zoomedPanelForeground: { - margin: 18, - border: border(colorScheme.highest, { overlay: true }), + margin: 16, + border: border(colorScheme.lowest, { overlay: true }), }, dock: { left: { From 1f72f9e18babcd8906d1d550ef4691831b3accaf Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 31 May 2023 12:59:28 -0400 Subject: [PATCH 017/101] Add un-hooked up zoomIcon styles --- styles/src/styleTree/tabBar.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index 61d30be7e03c8eceb026ea3f0fa1c39eab9cca60..ef1425cd1553c8b98a3d44707da99db5c3692bc5 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -31,6 +31,10 @@ export default function tabBar(colorScheme: ColorScheme) { iconClose: foreground(layer, "variant"), iconCloseActive: foreground(layer, "hovered"), + // Zoom Icons + iconZoom: foreground(layer, "variant"), + iconZoomActive: foreground(layer, "accent"), + // Indicators iconConflict: foreground(layer, "warning"), iconDirty: foreground(layer, "accent"), From 893615236dd721a235bf8afc3fcb446f3dcd5721 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 12:45:48 -0700 Subject: [PATCH 018/101] Only include one border edge for zoomed panels --- crates/workspace/src/workspace.rs | 56 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d7219970535405a2c29d5bf1367877915d80f5ec..1b2fdc80f4e58ca982b1d274795b12fc835f8ac9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3288,32 +3288,36 @@ impl View for Workspace { enum ZoomBackground {} let zoomed = zoomed.upgrade(cx)?; - let mut foreground_style; - match self.zoomed_position { - Some(DockPosition::Left) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.left = 0.; - foreground_style.margin.top = 0.; - foreground_style.margin.bottom = 0.; - } - Some(DockPosition::Right) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.right = 0.; - foreground_style.margin.top = 0.; - foreground_style.margin.bottom = 0.; - } - Some(DockPosition::Bottom) => { - foreground_style = - theme.workspace.zoomed_panel_foreground; - foreground_style.margin.left = 0.; - foreground_style.margin.right = 0.; - foreground_style.margin.bottom = 0.; - } - None => { - foreground_style = - theme.workspace.zoomed_pane_foreground; + let mut foreground_style = + theme.workspace.zoomed_pane_foreground; + if let Some(zoomed_dock_position) = self.zoomed_position { + foreground_style = + theme.workspace.zoomed_panel_foreground; + let margin = foreground_style.margin.top; + let border = foreground_style.border.top; + + // Only include a margin and border on the opposite side. + foreground_style.margin.top = 0.; + foreground_style.margin.left = 0.; + foreground_style.margin.bottom = 0.; + foreground_style.margin.right = 0.; + foreground_style.border.top = false; + foreground_style.border.left = false; + foreground_style.border.bottom = false; + foreground_style.border.right = false; + match zoomed_dock_position { + DockPosition::Left => { + foreground_style.margin.right = margin; + foreground_style.border.right = border; + } + DockPosition::Right => { + foreground_style.margin.left = margin; + foreground_style.border.left = border; + } + DockPosition::Bottom => { + foreground_style.margin.top = margin; + foreground_style.border.top = border; + } } } From 3f6aa94a5f4f3c49457f7c4fb96ac3a46d34f8e3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 12:51:57 -0700 Subject: [PATCH 019/101] Use active color for zoom button in a zoomed pane --- crates/terminal_view/src/terminal_panel.rs | 2 ++ crates/workspace/src/pane.rs | 6 +++++- styles/src/styleTree/tabBar.ts | 7 +++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e45459e68363169b1bd0b3f2cc8b44621e078271..ac3875af9e10704be06a7a2a047f86ec1fe992b6 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -70,6 +70,7 @@ impl TerminalPanel { .with_child(Pane::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(( "New Terminal".into(), Some(Box::new(workspace::NewTerminal)), @@ -94,6 +95,7 @@ impl TerminalPanel { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 921ae5e0100d45ced85f1664dfe9a1acf534892a..80a0256389d6799f37e2e73d0b4b527818a33690 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -268,6 +268,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(("New...".into(), None)), cx, |pane, cx| pane.deploy_new_menu(cx), @@ -277,6 +278,7 @@ impl Pane { .with_child(Self::render_tab_bar_button( 1, "icons/split_12.svg", + false, Some(("Split Pane".into(), None)), cx, |pane, cx| pane.deploy_split_menu(cx), @@ -290,6 +292,7 @@ impl Pane { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), @@ -1401,6 +1404,7 @@ impl Pane { pub fn render_tab_bar_button)>( index: usize, icon: &'static str, + active: bool, tooltip: Option<(String, Option>)>, cx: &mut ViewContext, on_click: F, @@ -1410,7 +1414,7 @@ impl Pane { let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; - let style = theme.pane_button.style_for(mouse_state, false); + let style = theme.pane_button.style_for(mouse_state, active); Svg::new(icon) .with_color(style.color) .constrained() diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts index ef1425cd1553c8b98a3d44707da99db5c3692bc5..39a1ef0407ce92b966e45f19f17a89c3c248c59c 100644 --- a/styles/src/styleTree/tabBar.ts +++ b/styles/src/styleTree/tabBar.ts @@ -31,10 +31,6 @@ export default function tabBar(colorScheme: ColorScheme) { iconClose: foreground(layer, "variant"), iconCloseActive: foreground(layer, "hovered"), - // Zoom Icons - iconZoom: foreground(layer, "variant"), - iconZoomActive: foreground(layer, "accent"), - // Indicators iconConflict: foreground(layer, "warning"), iconDirty: foreground(layer, "accent"), @@ -98,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) { hover: { color: foreground(layer, "hovered"), }, + active: { + color: foreground(layer, "accent"), + } }, paneButtonContainer: { background: tab.background, From c48fed26bdebdcb7573e7a06b1ac4fcd0068dc9e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 13:05:50 -0700 Subject: [PATCH 020/101] When revealing items, zoom-out any inactive panes --- crates/workspace/src/workspace.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1b2fdc80f4e58ca982b1d274795b12fc835f8ac9..3d55a78e88e228c7c8619ffb2146bbc5921be8bb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1705,8 +1705,8 @@ impl Workspace { cx: &mut ViewContext, ) { // If a center pane is zoomed, unzoom it. - if except_position.is_some() { - for pane in &self.panes { + for pane in &self.panes { + if pane != &self.active_pane { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); } } From 193474a346100b4496bc074b3a1efcf5c7cf611a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 13:07:20 -0700 Subject: [PATCH 021/101] :art: --- crates/workspace/src/workspace.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3d55a78e88e228c7c8619ffb2146bbc5921be8bb..b00b8f1102fa0a7bdac0d3d73c182f3751733113 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1701,7 +1701,7 @@ impl Workspace { fn dismiss_zoomed_items_to_reveal( &mut self, - except_position: Option, + dock_to_reveal: Option, cx: &mut ViewContext, ) { // If a center pane is zoomed, unzoom it. @@ -1715,7 +1715,7 @@ impl Workspace { let mut focus_center = false; for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { dock.update(cx, |dock, cx| { - if Some(dock.position()) != except_position { + if Some(dock.position()) != dock_to_reveal { if let Some(panel) = dock.active_panel() { if panel.is_zoomed(cx) { focus_center |= panel.has_focus(cx); @@ -1730,7 +1730,7 @@ impl Workspace { cx.focus_self(); } - if self.zoomed_position != except_position { + if self.zoomed_position != dock_to_reveal { self.zoomed = None; self.zoomed_position = None; } From ef80b539d150d67faa0dc821077364975eafea36 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 13:56:41 -0700 Subject: [PATCH 022/101] Fix notification styling, minimize database reads --- crates/workspace/src/notifications.rs | 38 ++++++++++++------ crates/workspace/src/workspace.rs | 57 +++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 4437a960ef250e1a025c3fb1255826823cd42fda..1060227d4ace10dc429af586336b75b7db3d36c7 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -33,7 +33,7 @@ impl From<&dyn NotificationHandle> for AnyViewHandle { } } -struct NotificationTracker { +pub(crate) struct NotificationTracker { notifications_sent: HashMap>, } @@ -65,8 +65,7 @@ impl Workspace { id: usize, cx: &ViewContext, ) -> bool { - cx - .global::() + cx.global::() .get(&TypeId::of::()) .map(|ids| ids.contains(&id)) .unwrap_or(false) @@ -78,8 +77,7 @@ impl Workspace { cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { - if !self.has_shown_notification_once::(id, cx) - { + if !self.has_shown_notification_once::(id, cx) { cx.update_global::(|tracker, _| { let entry = tracker.entry(TypeId::of::()).or_default(); entry.push(id); @@ -167,7 +165,7 @@ pub mod simple_message_notification { elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, impl_actions, platform::{CursorStyle, MouseButton}, - AppContext, Element, Entity, View, ViewContext, + AnyElement, AppContext, Element, Entity, View, ViewContext, fonts::TextStyle, }; use menu::Cancel; use serde::Deserialize; @@ -195,8 +193,13 @@ pub mod simple_message_notification { ) } + enum NotificationMessage { + Text(Cow<'static, str>), + Element(fn(TextStyle, &AppContext) -> AnyElement), + } + pub struct MessageNotification { - message: Cow<'static, str>, + message: NotificationMessage, on_click: Option)>>, click_message: Option>, } @@ -215,7 +218,16 @@ pub mod simple_message_notification { S: Into>, { Self { - message: message.into(), + message: NotificationMessage::Text(message.into()), + on_click: None, + click_message: None, + } + } + + pub fn new_element(message: fn(TextStyle, &AppContext) -> AnyElement) -> MessageNotification + { + Self { + message: NotificationMessage::Element(message), on_click: None, click_message: None, } @@ -254,7 +266,12 @@ pub mod simple_message_notification { enum MessageNotificationTag {} let click_message = self.click_message.clone(); - let message = self.message.clone(); + let message = match &self.message { + NotificationMessage::Text(text) => { + Text::new(text.to_owned(), theme.message.text.clone()).into_any() + } + NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), + }; let on_click = self.on_click.clone(); let has_click_action = on_click.is_some(); @@ -262,8 +279,7 @@ pub mod simple_message_notification { .with_child( Flex::row() .with_child( - Text::new(message, theme.message.text.clone()) - .contained() + message.contained() .with_style(theme.message.container) .aligned() .top() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 292ec28abc664335caa97d0402d62e3fbc6e5215..823c9c9294ef56c82c65df0c00f0ecde2f9f4e45 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,7 @@ use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, ZED_APP_VERSION, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -60,7 +60,7 @@ use std::{ }; use crate::{ - notifications::simple_message_notification::MessageNotification, + notifications::{simple_message_notification::MessageNotification, NotificationTracker}, persistence::model::{ DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, }, @@ -81,7 +81,7 @@ use serde::Deserialize; use shared_screen::SharedScreen; use status_bar::StatusBar; pub use status_bar::StatusItemView; -use theme::Theme; +use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::{async_iife, channel::ZedVersion, paths, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; @@ -3193,6 +3193,7 @@ async fn open_items( fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppContext) { const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system"; const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key"; + const MESSAGE_ID: usize = 2; if workspace .read_with(cx, |workspace, cx| { @@ -3205,7 +3206,7 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo { return true; } - workspace.has_shown_notification_once::(2, cx) + workspace.has_shown_notification_once::(MESSAGE_ID, cx) }) .unwrap_or(false) { @@ -3218,6 +3219,24 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo .flatten() .is_some() { + if !workspace + .read_with(cx, |workspace, cx| { + workspace.has_shown_notification_once::(MESSAGE_ID, cx) + }) + .unwrap_or(false) + { + cx.update(|cx| { + cx.update_global::(|tracker, _| { + let entry = tracker + .entry(TypeId::of::()) + .or_default(); + if !entry.contains(&MESSAGE_ID) { + entry.push(MESSAGE_ID); + } + }); + }); + } + return; } @@ -3232,11 +3251,33 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo workspace .update(cx, |workspace, cx| { workspace.show_notification_once(2, cx, |cx| { + + cx.add_view(|_| { - MessageNotification::new( - "Looking for the dock? Try 'ctrl-`'!\n'shift-escape' now zooms your pane", - ) - .with_click_message("Click to read more about the new panel system") + MessageNotification::new_element(|text, _| { + Text::new( + "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.", + text, + ) + .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| { + let code_span_background_color = settings::get::(cx) + .theme + .editor + .document_highlight_read_background; + + scene.push_quad(gpui::Quad { + bounds, + background: Some(code_span_background_color), + border: Default::default(), + corner_radius: 2.0, + }) + }) + .into_any() + }) + // MessageNotification::new_( + // "Looking for the dock? Try 'ctrl-`'!\n'shift-escape' now zooms your pane", + // ) + .with_click_message("Read more about the new panel system") .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) }) }) From ed0b9acb0a5d16b363b42efbf7f1e2b30a2b95e7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:06:36 -0700 Subject: [PATCH 023/101] Add panic if version is 0.91 --- crates/workspace/src/notifications.rs | 11 +++++++---- crates/workspace/src/pane.rs | 1 - crates/workspace/src/workspace.rs | 14 ++------------ 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1060227d4ace10dc429af586336b75b7db3d36c7..1e3c6044a1651101939453d2ee2e5c6b90df3564 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -163,9 +163,10 @@ pub mod simple_message_notification { use gpui::{ actions, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, + fonts::TextStyle, impl_actions, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Entity, View, ViewContext, fonts::TextStyle, + AnyElement, AppContext, Element, Entity, View, ViewContext, }; use menu::Cancel; use serde::Deserialize; @@ -224,8 +225,9 @@ pub mod simple_message_notification { } } - pub fn new_element(message: fn(TextStyle, &AppContext) -> AnyElement) -> MessageNotification - { + pub fn new_element( + message: fn(TextStyle, &AppContext) -> AnyElement, + ) -> MessageNotification { Self { message: NotificationMessage::Element(message), on_click: None, @@ -279,7 +281,8 @@ pub mod simple_message_notification { .with_child( Flex::row() .with_child( - message.contained() + message + .contained() .with_style(theme.message.container) .aligned() .top() diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fad8cd8864e1a55cec7d214201abb35eb60f9404..63602ffead64ae2429e3c626772dac35b9b2ef63 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -541,7 +541,6 @@ impl Pane { cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) }) .detach(); - if self.zoomed { cx.emit(Event::ZoomOut); } else if !self.items.is_empty() { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 823c9c9294ef56c82c65df0c00f0ecde2f9f4e45..3690db60d070c1fd9df90d16cda1d4c62c66a803 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3198,13 +3198,8 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo if workspace .read_with(cx, |workspace, cx| { let version = cx.global::().0; - if !version.contains("0.88") - && !version.contains("0.89") - && !version.contains("0.90") - && !version.contains("0.91") - && !version.contains("0.92") - { - return true; + if version.contains("0.91") { + panic!("Please remove the dock key binding change notification"); } workspace.has_shown_notification_once::(MESSAGE_ID, cx) }) @@ -3251,8 +3246,6 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo workspace .update(cx, |workspace, cx| { workspace.show_notification_once(2, cx, |cx| { - - cx.add_view(|_| { MessageNotification::new_element(|text, _| { Text::new( @@ -3274,9 +3267,6 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo }) .into_any() }) - // MessageNotification::new_( - // "Looking for the dock? Try 'ctrl-`'!\n'shift-escape' now zooms your pane", - // ) .with_click_message("Read more about the new panel system") .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST)) }) From b875d4ed40eac05bf6007a6d9f08f64ca65c446f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:10:50 -0700 Subject: [PATCH 024/101] Remove silly panic --- crates/workspace/src/workspace.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3690db60d070c1fd9df90d16cda1d4c62c66a803..73cc811e76402b5624afdb2e8ed27c89db355565 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3197,10 +3197,6 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo if workspace .read_with(cx, |workspace, cx| { - let version = cx.global::().0; - if version.contains("0.91") { - panic!("Please remove the dock key binding change notification"); - } workspace.has_shown_notification_once::(MESSAGE_ID, cx) }) .unwrap_or(false) From 705e36827c8e48dd0b2ccfb969806e41148ec935 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:16:37 -0700 Subject: [PATCH 025/101] add version check --- crates/util/src/channel.rs | 2 -- crates/workspace/src/workspace.rs | 12 ++++++++++-- crates/zed/src/main.rs | 1 - 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 2b45cb6b6611cb6a036f1cf61768fcd2f5dac184..274fd576a050076511c8c1253b7187fbd437e8c3 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -2,8 +2,6 @@ use std::env; use lazy_static::lazy_static; -pub struct ZedVersion(pub &'static str); - lazy_static! { pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) { env::var("ZED_RELEASE_CHANNEL") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 73cc811e76402b5624afdb2e8ed27c89db355565..7033530f96e080b8422031439648819ccf8cc9ad 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,7 @@ use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, UserStore, ZED_APP_VERSION, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -83,7 +83,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, channel::ZedVersion, paths, ResultExt}; +use util::{async_iife, paths, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -3197,6 +3197,14 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo if workspace .read_with(cx, |workspace, cx| { + let version = ZED_APP_VERSION + .or_else(|| cx.platform().app_version().ok()) + .map(|v| v.to_string()).unwrap_or_default(); + + if !version.contains("0.88") || !version.contains("0.89") || !version.contains("0.90") || !version.contains("0.91") { + return true; + } + workspace.has_shown_notification_once::(MESSAGE_ID, cx) }) .unwrap_or(false) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 558796807bedc4f8151faaa8cf0c6544ac5f5423..31f331ef93ef17eecb4870f3ed23c9f963a5b3aa 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -119,7 +119,6 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); - cx.set_global(util::channel::ZedVersion(env!("CARGO_PKG_VERSION"))); #[cfg(debug_assertions)] cx.set_global(StaffMode(true)); From 45b42c512d50b96f49c9722793572a0b484b2305 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:17:27 -0700 Subject: [PATCH 026/101] fmt --- crates/workspace/src/workspace.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7033530f96e080b8422031439648819ccf8cc9ad..c45cdf0c29ad9f103d11b0774891c86a7c62c54a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3199,9 +3199,14 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo .read_with(cx, |workspace, cx| { let version = ZED_APP_VERSION .or_else(|| cx.platform().app_version().ok()) - .map(|v| v.to_string()).unwrap_or_default(); + .map(|v| v.to_string()) + .unwrap_or_default(); - if !version.contains("0.88") || !version.contains("0.89") || !version.contains("0.90") || !version.contains("0.91") { + if !version.contains("0.88") + || !version.contains("0.89") + || !version.contains("0.90") + || !version.contains("0.91") + { return true; } From 27ef0e2b524755515eab87d37898f6759f15e2d3 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:21:38 -0700 Subject: [PATCH 027/101] De-morgans properly --- crates/workspace/src/workspace.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c45cdf0c29ad9f103d11b0774891c86a7c62c54a..5d1bc2993daa05b4f6da3ec39713f2f4a69affd9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3203,9 +3203,9 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo .unwrap_or_default(); if !version.contains("0.88") - || !version.contains("0.89") - || !version.contains("0.90") - || !version.contains("0.91") + && !version.contains("0.89") + && !version.contains("0.90") + && !version.contains("0.91") { return true; } From bf2016adf504637b168b3fca76d0e9fb955ea293 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 May 2023 14:22:49 -0700 Subject: [PATCH 028/101] Remove version check --- crates/workspace/src/workspace.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5d1bc2993daa05b4f6da3ec39713f2f4a69affd9..91824f585551ce34f6e0f0ebaa2ea4aadb0d1918 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -19,7 +19,7 @@ use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, ZED_APP_VERSION, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -3197,19 +3197,6 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo if workspace .read_with(cx, |workspace, cx| { - let version = ZED_APP_VERSION - .or_else(|| cx.platform().app_version().ok()) - .map(|v| v.to_string()) - .unwrap_or_default(); - - if !version.contains("0.88") - && !version.contains("0.89") - && !version.contains("0.90") - && !version.contains("0.91") - { - return true; - } - workspace.has_shown_notification_once::(MESSAGE_ID, cx) }) .unwrap_or(false) From a2ab7c9eb95f4970b0c2222e4494c83e2cb2779b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 14:42:15 -0700 Subject: [PATCH 029/101] Respect project-specific settings for copilot --- crates/copilot/src/copilot.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 32 +++++++++++-------- crates/editor/src/editor.rs | 15 +++++---- crates/language/src/language_settings.rs | 10 ++++-- crates/project/src/project.rs | 35 +++++++++++---------- crates/project/src/worktree.rs | 2 +- 6 files changed, 56 insertions(+), 40 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 88b0aebd17127765535db8bf8966dd630e95b851..108422264810c27603f937072caf44cc07b5cb49 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -318,7 +318,7 @@ impl Copilot { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); - if all_language_settings(cx).copilot_enabled(None, None) { + if all_language_settings(None, cx).copilot_enabled(None, None) { if matches!(self.server, CopilotServer::Disabled) { let start_task = cx .spawn({ diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 686b3d2dfb3dc222433166f3aeed20f5d869be33..4cb7c15bd94623c1223f2dc64582e80962a82b3a 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -9,7 +9,10 @@ use gpui::{ AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::language_settings::{self, all_language_settings, AllLanguageSettings}; +use language::{ + language_settings::{self, all_language_settings, AllLanguageSettings}, + File, +}; use settings::{update_settings_file, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; @@ -27,7 +30,7 @@ pub struct CopilotButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, language: Option>, - path: Option>, + file: Option>, fs: Arc, } @@ -41,7 +44,7 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let all_language_settings = &all_language_settings(cx); + let all_language_settings = all_language_settings(None, cx); if !all_language_settings.copilot.feature_enabled { return Empty::new().into_any(); } @@ -165,7 +168,7 @@ impl CopilotButton { editor_subscription: None, editor_enabled: None, language: None, - path: None, + file: None, fs, } } @@ -212,9 +215,9 @@ impl CopilotButton { let settings = settings::get::(cx); - if let Some(path) = self.path.as_ref() { - let path_enabled = settings.copilot_enabled_for_path(path); - let path = path.clone(); + if let Some(file) = &self.file { + let path = file.path().clone(); + let path_enabled = settings.copilot_enabled_for_path(&path); menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for This Path", @@ -279,14 +282,16 @@ impl CopilotButton { let language_name = snapshot .language_at(suggestion_anchor) .map(|language| language.name()); - let path = snapshot.file_at(suggestion_anchor).map(|file| file.path()); + let file = snapshot.file_at(suggestion_anchor).cloned(); self.editor_enabled = Some( - all_language_settings(cx) - .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), + all_language_settings(self.file.as_ref().map(|f| f.as_ref()), cx).copilot_enabled( + language_name.as_deref(), + file.as_ref().map(|file| file.path().as_ref()), + ), ); self.language = language_name; - self.path = path.cloned(); + self.file = file; cx.notify() } @@ -363,14 +368,15 @@ async fn configure_disabled_globs( } fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None); + let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); update_settings_file::(fs, cx, move |file| { file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }); } fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); + let show_copilot_suggestions = + all_language_settings(None, cx).copilot_enabled(Some(&language), None); update_settings_file::(fs, cx, move |file| { file.languages .entry(language) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f5d109e15bd2138fba6914e058b575a5bdf42e80..4a9237f5632ef73da74b97c68fa45da6f07b4626 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3207,12 +3207,12 @@ impl Editor { snapshot: &MultiBufferSnapshot, cx: &mut ViewContext, ) -> bool { - let path = snapshot.file_at(location).map(|file| file.path().as_ref()); + let file = snapshot.file_at(location); let language_name = snapshot .language_at(location) .map(|language| language.name()); - let settings = all_language_settings(cx); - settings.copilot_enabled(language_name.as_deref(), path) + let settings = all_language_settings(file.map(|f| f.as_ref() as _), cx); + settings.copilot_enabled(language_name.as_deref(), file.map(|f| f.path().as_ref())) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -7076,11 +7076,13 @@ impl Editor { }; // If None, we are in a file without an extension - let file_extension = file_extension.or(self + let file = self .buffer .read(cx) .as_singleton() - .and_then(|b| b.read(cx).file()) + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() .and_then(|file| Path::new(file.file_name(cx)).extension()) .and_then(|e| e.to_str()) .map(|a| a.to_string())); @@ -7091,7 +7093,8 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); let telemetry_settings = *settings::get::(cx); - let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None); + let copilot_enabled = + all_language_settings(file.map(|f| f.as_ref()), cx).copilot_enabled(None, None); let copilot_enabled_for_language = self .buffer .read(cx) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 70aaca03c408ab0a9995fee88f54144f4ab997d9..e4e283bd7e6743cc17bf858f1e3f6d9964946eeb 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -26,8 +26,14 @@ pub fn language_settings<'a>( .language(language) } -pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { - settings::get::(cx) +pub fn all_language_settings<'a>( + file: Option<&dyn File>, + cx: &'a AppContext, +) -> &'a AllLanguageSettings { + settings::get_local::( + file.map(|f| (f.worktree_id(), f.path().as_ref())), + cx, + ) } #[derive(Debug, Clone)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3cc4a181c55ad18315204c780aa6c6a6da337666..c871bf0b96ce05394af514a50e57dba8ba9e71f8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,7 +28,7 @@ use gpui::{ ModelHandle, Task, WeakModelHandle, }; use language::{ - language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, + language_settings::{language_settings, FormatOnSave, Formatter}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -689,18 +689,15 @@ impl Project { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let settings = all_language_settings(cx); - let mut language_servers_to_start = Vec::new(); for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { - if settings - .language(Some(&language.name())) - .enable_language_server - { + let settings = + language_settings(Some(language.name().as_ref()), Some(file), cx); + if settings.enable_language_server { language_servers_to_start.push((file.worktree.clone(), language.clone())); } } @@ -708,18 +705,22 @@ impl Project { } let mut language_servers_to_stop = Vec::new(); - for language in self.languages.to_vec() { - for lsp_adapter in language.lsp_adapters() { - if !settings - .language(Some(&language.name())) + let languages = self.languages.to_vec(); + for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { + let language = languages.iter().find(|l| { + l.lsp_adapters() + .iter() + .any(|adapter| &adapter.name == started_lsp_name) + }); + if let Some(language) = language { + let worktree = self.worktree_for_id(*worktree_id, cx); + let file = worktree.and_then(|tree| tree.update(cx, |tree, cx| tree.root_file(cx))); + // let settings = + // language_settings(Some(language.name().as_ref()), Some(file), cx); + if !language_settings(Some(&language.name()), file.as_ref().map(|f| f as _), cx) .enable_language_server { - let lsp_name = &lsp_adapter.name; - for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { - if lsp_name == started_lsp_name { - language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); - } - } + language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 7432eb8d40b5ce7da02b3fb06c007aaa74de1730..5d121adc63c39fa433bdebeea5fb4fddf06f52b6 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -679,7 +679,7 @@ impl Worktree { } pub fn root_file(&self, cx: &mut ModelContext) -> Option { - let entry = self.entry_for_path("")?; + let entry = self.root_entry()?; Some(File { worktree: cx.handle(), path: entry.path.clone(), From 38078b93cc1361b58ef4529058ab3e62cc2dc3d3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 14:44:48 -0700 Subject: [PATCH 030/101] v0.90.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 3751b34d3156da64fc0efea74e707fc9222bb324..2b340cf46a917f4faf0f820fb67123a3ceca3877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8777,7 +8777,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.89.0" +version = "0.90.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e96dff122186cb07e17eff54ff909d36c1454731..c45991cf11fc7a8ed9e654781d6d144a1d6795d5 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.89.0" +version = "0.90.0" publish = false [lib] From 03a351fb26a454c1dbd8eec93a4a05f50f4768e0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 14:57:04 -0700 Subject: [PATCH 031/101] Make language settings accessors take an arc dyn file --- crates/copilot/src/copilot.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 2 +- crates/editor/src/editor.rs | 5 ++-- crates/editor/src/multi_buffer.rs | 4 +-- crates/language/src/buffer.rs | 8 ++---- crates/language/src/language_settings.rs | 4 +-- crates/project/src/lsp_command.rs | 3 +-- crates/project/src/project.rs | 30 +++++++++------------ crates/project/src/project_tests.rs | 20 ++++++++------ crates/project/src/worktree.rs | 17 ++++-------- 10 files changed, 41 insertions(+), 54 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 108422264810c27603f937072caf44cc07b5cb49..9343bd33f6092fb289609c956f7104b99baecfeb 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -787,7 +787,7 @@ impl Copilot { let position = position.to_point_utf16(buffer); let settings = language_settings( buffer.language_at(position).map(|l| l.name()).as_deref(), - buffer.file().map(|f| f.as_ref()), + buffer.file(), cx, ); let tab_size = settings.tab_size; diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 4cb7c15bd94623c1223f2dc64582e80962a82b3a..27fd2dcb7c63b596fce54b0a353d3f4c3d138437 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -285,7 +285,7 @@ impl CopilotButton { let file = snapshot.file_at(suggestion_anchor).cloned(); self.editor_enabled = Some( - all_language_settings(self.file.as_ref().map(|f| f.as_ref()), cx).copilot_enabled( + all_language_settings(self.file.as_ref(), cx).copilot_enabled( language_name.as_deref(), file.as_ref().map(|file| file.path().as_ref()), ), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4a9237f5632ef73da74b97c68fa45da6f07b4626..1b62192999f480f427d880abb12347a60203545f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3211,7 +3211,7 @@ impl Editor { let language_name = snapshot .language_at(location) .map(|language| language.name()); - let settings = all_language_settings(file.map(|f| f.as_ref() as _), cx); + let settings = all_language_settings(file, cx); settings.copilot_enabled(language_name.as_deref(), file.map(|f| f.path().as_ref())) } @@ -7093,8 +7093,7 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); let telemetry_settings = *settings::get::(cx); - let copilot_enabled = - all_language_settings(file.map(|f| f.as_ref()), cx).copilot_enabled(None, None); + let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None); let copilot_enabled_for_language = self .buffer .read(cx) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 597feb832b07c80ed33bdf578377c720c16a393e..1cf30a1676bc46f1ba08d4b3597eb1b9425958b3 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1382,7 +1382,7 @@ impl MultiBuffer { if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { let buffer = buffer.read(cx); language = buffer.language_at(offset).map(|l| l.name()); - file = buffer.file().map(|f| f.as_ref()); + file = buffer.file(); } language_settings(language.as_deref(), file, cx) } @@ -2795,7 +2795,7 @@ impl MultiBufferSnapshot { let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { language = buffer.language_at(offset).map(|l| l.name()); - file = buffer.file().map(|f| f.as_ref()); + file = buffer.file(); } language_settings(language.as_deref(), file, cx) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ddec085e2a622ca84190c5acec9c891512a80b2f..90a86d065cc448e13eff3c231680ab44e21a3473 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1808,11 +1808,7 @@ impl BufferSnapshot { pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { let language_name = self.language_at(position).map(|language| language.name()); - let settings = language_settings( - language_name.as_deref(), - self.file().map(|f| f.as_ref()), - cx, - ); + let settings = language_settings(language_name.as_deref(), self.file(), cx); if settings.hard_tabs { IndentSize::tab() } else { @@ -2139,7 +2135,7 @@ impl BufferSnapshot { let language = self.language_at(position); language_settings( language.map(|l| l.name()).as_deref(), - self.file.as_ref().map(AsRef::as_ref), + self.file.as_ref(), cx, ) } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index e4e283bd7e6743cc17bf858f1e3f6d9964946eeb..31d819b4493046c83c0cd05147df1252187066d3 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -16,7 +16,7 @@ pub fn init(cx: &mut AppContext) { pub fn language_settings<'a>( language: Option<&str>, - file: Option<&dyn File>, + file: Option<&Arc>, cx: &'a AppContext, ) -> &'a LanguageSettings { settings::get_local::( @@ -27,7 +27,7 @@ pub fn language_settings<'a>( } pub fn all_language_settings<'a>( - file: Option<&dyn File>, + file: Option<&Arc>, cx: &'a AppContext, ) -> &'a AllLanguageSettings { settings::get_local::( diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0a8b5f8a2e52f6a33e14bd5bc4a01ebe3dba7601..f34956653509488c2d9e4a147dbdba3a836480cd 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1717,8 +1717,7 @@ impl LspCommand for OnTypeFormatting { let tab_size = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - let file = buffer.file().map(|f| f.as_ref()); - language_settings(language_name.as_deref(), file, cx).tab_size + language_settings(language_name.as_deref(), buffer.file(), cx).tab_size }); Ok(Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c871bf0b96ce05394af514a50e57dba8ba9e71f8..a106632e3662521cbbc17bf55fe9c933c405ecfd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -693,12 +693,14 @@ impl Project { for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); - if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) - { + if let Some((file, language)) = buffer.file().zip(buffer.language()) { let settings = language_settings(Some(language.name().as_ref()), Some(file), cx); if settings.enable_language_server { - language_servers_to_start.push((file.worktree.clone(), language.clone())); + if let Some(file) = File::from_dyn(Some(file)) { + language_servers_to_start + .push((file.worktree.clone(), language.clone())); + } } } } @@ -714,10 +716,10 @@ impl Project { }); if let Some(language) = language { let worktree = self.worktree_for_id(*worktree_id, cx); - let file = worktree.and_then(|tree| tree.update(cx, |tree, cx| tree.root_file(cx))); - // let settings = - // language_settings(Some(language.name().as_ref()), Some(file), cx); - if !language_settings(Some(&language.name()), file.as_ref().map(|f| f as _), cx) + let file = worktree.and_then(|tree| { + tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) + }); + if !language_settings(Some(&language.name()), file.as_ref(), cx) .enable_language_server { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); @@ -2362,8 +2364,8 @@ impl Project { Some(&language.name()), worktree .update(cx, |tree, cx| tree.root_file(cx)) - .as_ref() - .map(|f| f as _), + .map(|f| f as _) + .as_ref(), cx, ) .enable_language_server @@ -3464,12 +3466,7 @@ impl Project { for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { let settings = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - language_settings( - language_name.as_deref(), - buffer.file().map(|f| f.as_ref()), - cx, - ) - .clone() + language_settings(language_name.as_deref(), buffer.file(), cx).clone() }); let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -4481,10 +4478,9 @@ impl Project { let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { let position = position.to_point_utf16(buffer); let language_name = buffer.language_at(position).map(|l| l.name()); - let file = buffer.file().map(|f| f.as_ref()); ( position, - language_settings(language_name.as_deref(), file, cx).tab_size, + language_settings(language_name.as_deref(), buffer.file(), cx).tab_size, ) }); self.request_lsp( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 577b22d7309e3afeba8ae1f5a5ccb376c5aeeffb..ee6752412dbb9e2b68fbae69f736826f833fd104 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -99,18 +99,22 @@ async fn test_managing_project_specific_settings( let settings_a = language_settings( None, - Some(&File::for_entry( - tree.entry_for_path("a/a.rs").unwrap().clone(), - worktree.clone(), - )), + Some( + &(File::for_entry( + tree.entry_for_path("a/a.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), cx, ); let settings_b = language_settings( None, - Some(&File::for_entry( - tree.entry_for_path("b/b.rs").unwrap().clone(), - worktree.clone(), - )), + Some( + &(File::for_entry( + tree.entry_for_path("b/b.rs").unwrap().clone(), + worktree.clone(), + ) as _), + ), cx, ); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 5d121adc63c39fa433bdebeea5fb4fddf06f52b6..a33a0fc050177db96dfbb56a901331bb6928e5ff 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -678,16 +678,9 @@ impl Worktree { } } - pub fn root_file(&self, cx: &mut ModelContext) -> Option { + pub fn root_file(&self, cx: &mut ModelContext) -> Option> { let entry = self.root_entry()?; - Some(File { - worktree: cx.handle(), - path: entry.path.clone(), - mtime: entry.mtime, - entry_id: entry.id, - is_local: self.is_local(), - is_deleted: false, - }) + Some(File::for_entry(entry.clone(), cx.handle())) } } @@ -2463,15 +2456,15 @@ impl language::LocalFile for File { } impl File { - pub fn for_entry(entry: Entry, worktree: ModelHandle) -> Self { - Self { + pub fn for_entry(entry: Entry, worktree: ModelHandle) -> Arc { + Arc::new(Self { worktree, path: entry.path.clone(), mtime: entry.mtime, entry_id: entry.id, is_local: true, is_deleted: false, - } + }) } pub fn from_proto( From 0dd7694ff531a5ae049fd754ddef5787a2a76e11 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 15:10:30 -0700 Subject: [PATCH 032/101] Make language_settings take a language, not a language name --- crates/copilot/src/copilot.rs | 6 +---- crates/copilot_button/src/copilot_button.rs | 27 +++++++++------------ crates/editor/src/display_map.rs | 7 +++--- crates/editor/src/editor.rs | 6 ++--- crates/editor/src/multi_buffer.rs | 8 +++--- crates/language/src/buffer.rs | 10 ++------ crates/language/src/language_settings.rs | 11 +++++---- crates/project/src/lsp_command.rs | 3 +-- crates/project/src/project.rs | 16 +++++------- crates/zed/src/languages/yaml.rs | 7 ++++-- 10 files changed, 41 insertions(+), 60 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9343bd33f6092fb289609c956f7104b99baecfeb..7834603552fa14a36d81109c2a8030a8fef4efee 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -785,11 +785,7 @@ impl Copilot { let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); let position = position.to_point_utf16(buffer); - let settings = language_settings( - buffer.language_at(position).map(|l| l.name()).as_deref(), - buffer.file(), - cx, - ); + let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); let tab_size = settings.tab_size; let hard_tabs = settings.hard_tabs; let relative_path = buffer diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 27fd2dcb7c63b596fce54b0a353d3f4c3d138437..e34fddd9b9c5f200bf5bb24120089208bba05a3e 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -11,7 +11,7 @@ use gpui::{ }; use language::{ language_settings::{self, all_language_settings, AllLanguageSettings}, - File, + File, Language, }; use settings::{update_settings_file, SettingsStore}; use std::{path::Path, sync::Arc}; @@ -29,7 +29,7 @@ pub struct CopilotButton { popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, - language: Option>, + language: Option>, file: Option>, fs: Arc, } @@ -200,14 +200,13 @@ impl CopilotButton { if let Some(language) = self.language.clone() { let fs = fs.clone(); - let language_enabled = - language_settings::language_settings(Some(language.as_ref()), None, cx) - .show_copilot_suggestions; + let language_enabled = language_settings::language_settings(Some(&language), None, cx) + .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for {}", if language_enabled { "Hide" } else { "Show" }, - language + language.name() ), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), )); @@ -279,18 +278,14 @@ impl CopilotButton { let editor = editor.read(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); let suggestion_anchor = editor.selections.newest_anchor().start; - let language_name = snapshot - .language_at(suggestion_anchor) - .map(|language| language.name()); + let language = snapshot.language_at(suggestion_anchor); let file = snapshot.file_at(suggestion_anchor).cloned(); self.editor_enabled = Some( - all_language_settings(self.file.as_ref(), cx).copilot_enabled( - language_name.as_deref(), - file.as_ref().map(|file| file.path().as_ref()), - ), + all_language_settings(self.file.as_ref(), cx) + .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())), ); - self.language = language_name; + self.language = language.cloned(); self.file = file; cx.notify() @@ -374,12 +369,12 @@ fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { }); } -fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { +fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(Some(&language), None); update_settings_file::(fs, cx, move |file| { file.languages - .entry(language) + .entry(language.name()) .or_default() .show_copilot_suggestions = Some(!show_copilot_suggestions); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f8d6f0bc406a497f13e36f320ed461aff1ddec60..b0483db68dcb781619188791b3f9b3630e902584 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -272,12 +272,11 @@ impl DisplayMap { } fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { - let language_name = buffer + let language = buffer .read(cx) .as_singleton() - .and_then(|buffer| buffer.read(cx).language()) - .map(|language| language.name()); - language_settings(language_name.as_deref(), None, cx).tab_size + .and_then(|buffer| buffer.read(cx).language()); + language_settings(language.as_deref(), None, cx).tab_size } #[cfg(test)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1b62192999f480f427d880abb12347a60203545f..9a9f36c577a1712a6e999d418761079edc07dc8d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3208,11 +3208,9 @@ impl Editor { cx: &mut ViewContext, ) -> bool { let file = snapshot.file_at(location); - let language_name = snapshot - .language_at(location) - .map(|language| language.name()); + let language = snapshot.language_at(location); let settings = all_language_settings(file, cx); - settings.copilot_enabled(language_name.as_deref(), file.map(|f| f.path().as_ref())) + settings.copilot_enabled(language, file.map(|f| f.path().as_ref())) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 1cf30a1676bc46f1ba08d4b3597eb1b9425958b3..b7b2b89c8ca312667bfb3b5988da8292fd33c6bc 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1381,10 +1381,10 @@ impl MultiBuffer { let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { let buffer = buffer.read(cx); - language = buffer.language_at(offset).map(|l| l.name()); + language = buffer.language_at(offset); file = buffer.file(); } - language_settings(language.as_deref(), file, cx) + language_settings(language.as_ref(), file, cx) } pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { @@ -2794,10 +2794,10 @@ impl MultiBufferSnapshot { let mut language = None; let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { - language = buffer.language_at(offset).map(|l| l.name()); + language = buffer.language_at(offset); file = buffer.file(); } - language_settings(language.as_deref(), file, cx) + language_settings(language, file, cx) } pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 90a86d065cc448e13eff3c231680ab44e21a3473..7acb36a92f8f87ed7f0d18b093a5cefe7068f6b1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1807,8 +1807,7 @@ impl BufferSnapshot { } pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { - let language_name = self.language_at(position).map(|language| language.name()); - let settings = language_settings(language_name.as_deref(), self.file(), cx); + let settings = language_settings(self.language_at(position), self.file(), cx); if settings.hard_tabs { IndentSize::tab() } else { @@ -2132,12 +2131,7 @@ impl BufferSnapshot { position: D, cx: &'a AppContext, ) -> &'a LanguageSettings { - let language = self.language_at(position); - language_settings( - language.map(|l| l.name()).as_deref(), - self.file.as_ref(), - cx, - ) + language_settings(self.language_at(position), self.file.as_ref(), cx) } pub fn language_scope_at(&self, position: D) -> Option { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 31d819b4493046c83c0cd05147df1252187066d3..b03dd6a6b9f363ab7c70a94aca0501b0ec54638f 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,4 +1,4 @@ -use crate::File; +use crate::{File, Language}; use anyhow::Result; use collections::HashMap; use globset::GlobMatcher; @@ -15,7 +15,7 @@ pub fn init(cx: &mut AppContext) { } pub fn language_settings<'a>( - language: Option<&str>, + language: Option<&Arc>, file: Option<&Arc>, cx: &'a AppContext, ) -> &'a LanguageSettings { @@ -23,7 +23,7 @@ pub fn language_settings<'a>( file.map(|f| (f.worktree_id(), f.path().as_ref())), cx, ) - .language(language) + .language(language.map(|l| l.name()).as_deref()) } pub fn all_language_settings<'a>( @@ -170,7 +170,7 @@ impl AllLanguageSettings { .any(|glob| glob.is_match(path)) } - pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { + pub fn copilot_enabled(&self, language: Option<&Arc>, path: Option<&Path>) -> bool { if !self.copilot.feature_enabled { return false; } @@ -181,7 +181,8 @@ impl AllLanguageSettings { } } - self.language(language_name).show_copilot_suggestions + self.language(language.map(|l| l.name()).as_deref()) + .show_copilot_suggestions } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index f34956653509488c2d9e4a147dbdba3a836480cd..7dab8eff30770cec4fc16bce9d1f1418b892fbc8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1716,8 +1716,7 @@ impl LspCommand for OnTypeFormatting { .await?; let tab_size = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), buffer.file(), cx).tab_size + language_settings(buffer.language(), buffer.file(), cx).tab_size }); Ok(Self { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a106632e3662521cbbc17bf55fe9c933c405ecfd..bec43385961647a92f530828e607f32876d673d0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -694,8 +694,7 @@ impl Project { if let Some(buffer) = buffer.upgrade(cx) { let buffer = buffer.read(cx); if let Some((file, language)) = buffer.file().zip(buffer.language()) { - let settings = - language_settings(Some(language.name().as_ref()), Some(file), cx); + let settings = language_settings(Some(language), Some(file), cx); if settings.enable_language_server { if let Some(file) = File::from_dyn(Some(file)) { language_servers_to_start @@ -719,9 +718,7 @@ impl Project { let file = worktree.and_then(|tree| { tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) }); - if !language_settings(Some(&language.name()), file.as_ref(), cx) - .enable_language_server - { + if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } } @@ -2361,7 +2358,7 @@ impl Project { cx: &mut ModelContext, ) { if !language_settings( - Some(&language.name()), + Some(&language), worktree .update(cx, |tree, cx| tree.root_file(cx)) .map(|f| f as _) @@ -3465,8 +3462,7 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { let settings = buffer.read_with(&cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - language_settings(language_name.as_deref(), buffer.file(), cx).clone() + language_settings(buffer.language(), buffer.file(), cx).clone() }); let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; @@ -4477,10 +4473,10 @@ impl Project { ) -> Task>> { let (position, tab_size) = buffer.read_with(cx, |buffer, cx| { let position = position.to_point_utf16(buffer); - let language_name = buffer.language_at(position).map(|l| l.name()); ( position, - language_settings(language_name.as_deref(), buffer.file(), cx).tab_size, + language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx) + .tab_size, ) }); self.request_lsp( diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index d66602ee04cb58da09d8b037020cb1fb483937fa..7f87a7caedb764588a698b5e863fbd418c3859ed 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, + language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, }; use node_runtime::NodeRuntime; use serde_json::Value; @@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter { } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { + let tab_size = all_language_settings(None, cx) + .language(Some("YAML")) + .tab_size; Some( future::ready(serde_json::json!({ "yaml": { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": language_settings(Some("YAML"), None, cx).tab_size, + "editor.tabSize": tab_size, } })) .boxed(), From 0d281c1b89531547879e666320728a47336b47a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 15:13:01 -0700 Subject: [PATCH 033/101] :art: --- crates/language/src/language_settings.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index b03dd6a6b9f363ab7c70a94aca0501b0ec54638f..332e789b4d7e364ab24c382a4daa9928291967cd 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -19,21 +19,16 @@ pub fn language_settings<'a>( file: Option<&Arc>, cx: &'a AppContext, ) -> &'a LanguageSettings { - settings::get_local::( - file.map(|f| (f.worktree_id(), f.path().as_ref())), - cx, - ) - .language(language.map(|l| l.name()).as_deref()) + let language_name = language.map(|l| l.name()); + all_language_settings(file, cx).language(language_name.as_deref()) } pub fn all_language_settings<'a>( file: Option<&Arc>, cx: &'a AppContext, ) -> &'a AllLanguageSettings { - settings::get_local::( - file.map(|f| (f.worktree_id(), f.path().as_ref())), - cx, - ) + let location = file.map(|f| (f.worktree_id(), f.path().as_ref())); + settings::get_local(location, cx) } #[derive(Debug, Clone)] From ae7606ce21d089ab10b751fc1a44720fe1592841 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 May 2023 15:38:26 -0700 Subject: [PATCH 034/101] collab 0.12.5 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b340cf46a917f4faf0f820fb67123a3ceca3877..07a859b058692c2ba7571f75b7cc9a9dd31374cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,7 +1246,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.12.4" +version = "0.12.5" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index cd06b9a70a253eb9668d2704880638c1eeabaaba..07d2cdc60a6439a96a1a93bd9d212e0207bb59ac 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.12.4" +version = "0.12.5" publish = false [[bin]] From e1a6dc90770ad5269ad5d464606028a6dadfe160 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:03:02 +0200 Subject: [PATCH 035/101] search: Add a default tooltip for empty searches Z-1074 --- crates/search/src/project_search.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2c22517e2040c87f3573cfdc099dcb2cab2ee070..0f8c9173bc3efbadcccdf47d2c83649ded58be85 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -25,7 +25,7 @@ use std::{ borrow::Cow, collections::HashSet, mem, - ops::Range, + ops::{Not, Range}, path::PathBuf, sync::Arc, }; @@ -242,7 +242,13 @@ impl View for ProjectSearchView { impl Item for ProjectSearchView { fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { - Some(self.query_editor.read(cx).text(cx).into()) + let query_text = self.query_editor.read(cx).text(cx); + + query_text + .is_empty() + .not() + .then(|| query_text.into()) + .or_else(|| Some("Project search".into())) } fn act_as_type<'a>( From 01621972c5d05c5f377487a1dcf8b39bb3977969 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 1 Jun 2023 12:31:37 +0200 Subject: [PATCH 036/101] Change "Project search" to "Project Search" --- crates/search/src/project_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0f8c9173bc3efbadcccdf47d2c83649ded58be85..27aac1762bec9f37389e0cba1a47d4e5a11016e0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -248,7 +248,7 @@ impl Item for ProjectSearchView { .is_empty() .not() .then(|| query_text.into()) - .or_else(|| Some("Project search".into())) + .or_else(|| Some("Project Search".into())) } fn act_as_type<'a>( From d8ce333cf8e347e25e9af9aca3a918e57da9c8af Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 1 Jun 2023 16:51:29 +0300 Subject: [PATCH 037/101] Remove wrong assertion --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 364c3cadaf6a3785b8c2cd32295629236c6bcd75..21feea2b4086433eb1b1e0c31788ce78d16464f4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -974,9 +974,8 @@ impl Workspace { let timestamp = entry.timestamp; match history.entry(project_path) { hash_map::Entry::Occupied(mut entry) => { - let (old_fs_path, old_timestamp) = entry.get(); + let (_, old_timestamp) = entry.get(); if ×tamp > old_timestamp { - assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history"); entry.insert((fs_path, timestamp)); } } From 51c82da8401219cbf8e7487bfd8fd381ca88082c Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 1 Jun 2023 12:04:45 -0400 Subject: [PATCH 038/101] Avoid blocking forever on startup if config files do not exist The files will still get created if the user opens their settings and saves, otherwise everything will transparently work Co-Authored-By: Antonio Scandurra Co-Authored-By: Max Brunsfeld --- crates/settings/src/settings_file.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 4c98dca51a4e661126843b28615e1cdb8a53accc..3505330eda6fb94cc4ded9d90f313c40a11e7866 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -66,15 +66,22 @@ pub fn watch_config_file( .spawn(async move { let events = fs.watch(&path, Duration::from_millis(100)).await; futures::pin_mut!(events); + + let contents = fs.load(&path).await.unwrap_or_default(); + if tx.unbounded_send(contents).is_err() { + return; + } + loop { + if events.next().await.is_none() { + break; + } + if let Ok(contents) = fs.load(&path).await { if !tx.unbounded_send(contents).is_ok() { break; } } - if events.next().await.is_none() { - break; - } } }) .detach(); From 2390815d67a5b30763a7e3a1fa3f53836347e6ef Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Jun 2023 10:09:04 -0700 Subject: [PATCH 039/101] Make settings store handle no user settings co-authored-by: max --- crates/settings/src/settings_store.rs | 191 +++++++++++++------------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index bb51fd1ed004911415abc292efc70c33f666685d..3fdb81dd45cfee2671314ecc902cb38958841579 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; use gpui::AppContext; use lazy_static::lazy_static; @@ -84,15 +84,26 @@ pub struct SettingsJsonSchemaParams<'a> { } /// A set of strongly-typed setting values defined via multiple JSON files. -#[derive(Default)] pub struct SettingsStore { setting_values: HashMap>, - default_deserialized_settings: Option, - user_deserialized_settings: Option, + default_deserialized_settings: serde_json::Value, + user_deserialized_settings: serde_json::Value, local_deserialized_settings: BTreeMap<(usize, Arc), serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } +impl Default for SettingsStore { + fn default() -> Self { + SettingsStore { + setting_values: Default::default(), + default_deserialized_settings: serde_json::json!({}), + user_deserialized_settings: serde_json::json!({}), + local_deserialized_settings: Default::default(), + tab_size_callback: Default::default(), + } + } +} + #[derive(Debug)] struct SettingValue { global_value: Option, @@ -136,27 +147,24 @@ impl SettingsStore { local_values: Vec::new(), })); - if let Some(default_settings) = &self.default_deserialized_settings { - if let Some(default_settings) = setting_value - .deserialize_setting(default_settings) + if let Some(default_settings) = setting_value + .deserialize_setting(&self.default_deserialized_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); + + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) .log_err() { - let mut user_values_stack = Vec::new(); - - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_values_stack = vec![user_settings]; - } - } + user_values_stack = vec![user_settings]; + } - if let Some(setting) = setting_value - .load_setting(&default_settings, &user_values_stack, cx) - .log_err() - { - setting_value.set_global_value(setting); - } + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .log_err() + { + setting_value.set_global_value(setting); } } } @@ -189,9 +197,7 @@ impl SettingsStore { /// This is only for debugging and reporting. For user-facing functionality, /// use the typed setting interface. pub fn untyped_user_settings(&self) -> &serde_json::Value { - self.user_deserialized_settings - .as_ref() - .unwrap_or(&serde_json::Value::Null) + &self.user_deserialized_settings } #[cfg(any(test, feature = "test-support"))] @@ -213,11 +219,7 @@ impl SettingsStore { cx: &AppContext, update: impl FnOnce(&mut T::FileContent), ) { - if self.user_deserialized_settings.is_none() { - self.set_user_settings("{}", cx).unwrap(); - } - let old_text = - serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap(); + let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap(); let new_text = self.new_text_for_update::(old_text, update); self.set_user_settings(&new_text, cx).unwrap(); } @@ -250,11 +252,7 @@ impl SettingsStore { .setting_values .get(&setting_type_id) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .deserialize_setting( - self.user_deserialized_settings - .as_ref() - .expect("no user settings loaded"), - ) + .deserialize_setting(&self.user_deserialized_settings) .unwrap_or_else(|e| { panic!( "could not deserialize setting type {} from user settings: {}", @@ -323,10 +321,14 @@ impl SettingsStore { default_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.default_deserialized_settings = - Some(parse_json_with_comments(default_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; + if settings.is_object() { + self.default_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Set the user settings via a JSON string. @@ -335,9 +337,14 @@ impl SettingsStore { user_settings_content: &str, cx: &AppContext, ) -> Result<()> { - self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); - self.recompute_values(None, cx)?; - Ok(()) + let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?; + if settings.is_object() { + self.user_deserialized_settings = settings; + self.recompute_values(None, cx)?; + Ok(()) + } else { + Err(anyhow!("settings must be an object")) + } } /// Add or remove a set of local settings via a JSON string. @@ -361,7 +368,6 @@ impl SettingsStore { /// Add or remove a set of local settings via a JSON string. pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> { - eprintln!("clearing local settings {root_id}"); self.local_deserialized_settings .retain(|k, _| k.0 != root_id); self.recompute_values(Some((root_id, "".as_ref())), cx)?; @@ -460,68 +466,63 @@ impl SettingsStore { let mut user_settings_stack = Vec::::new(); let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { - if let Some(default_settings) = &self.default_deserialized_settings { - let default_settings = setting_value.deserialize_setting(default_settings)?; + let default_settings = + setting_value.deserialize_setting(&self.default_deserialized_settings)?; - user_settings_stack.clear(); - paths_stack.clear(); + user_settings_stack.clear(); + paths_stack.clear(); - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_settings) = - setting_value.deserialize_setting(user_settings).log_err() - { - user_settings_stack.push(user_settings); - paths_stack.push(None); - } + if let Some(user_settings) = setting_value + .deserialize_setting(&self.user_deserialized_settings) + .log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } + + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() + { + setting_value.set_global_value(value); } + } - // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { - if let Some(value) = setting_value - .load_setting(&default_settings, &user_settings_stack, cx) - .log_err() - { - setting_value.set_global_value(value); + // Reload the local values for the setting. + for ((root_id, path), local_settings) in &self.local_deserialized_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_entry) = paths_stack.last() { + if let Some((prev_root_id, prev_path)) = prev_entry { + if root_id != prev_root_id || !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } } + break; } - // Reload the local values for the setting. - for ((root_id, path), local_settings) in &self.local_deserialized_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry { - if root_id != prev_root_id || !path.starts_with(prev_path) { - paths_stack.pop(); - user_settings_stack.pop(); - continue; - } - } - break; + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() + { + paths_stack.push(Some((*root_id, path.as_ref()))); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| { + *root_id != changed_root_id || !path.starts_with(changed_local_path) + }) { + continue; } - if let Some(local_settings) = - setting_value.deserialize_setting(&local_settings).log_err() + if let Some(value) = setting_value + .load_setting(&default_settings, &user_settings_stack, cx) + .log_err() { - paths_stack.push(Some((*root_id, path.as_ref()))); - user_settings_stack.push(local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.map_or( - false, - |(changed_root_id, changed_local_path)| { - *root_id != changed_root_id || !path.starts_with(changed_local_path) - }, - ) { - continue; - } - - if let Some(value) = setting_value - .load_setting(&default_settings, &user_settings_stack, cx) - .log_err() - { - setting_value.set_local_value(*root_id, path.clone(), value); - } + setting_value.set_local_value(*root_id, path.clone(), value); } } } From 40c6baf7cbd92bbe700c57e18d7bc442fe12eb71 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Jun 2023 10:49:27 -0700 Subject: [PATCH 040/101] Redraw the terminal on every wakeup co-authored-by: max --- crates/terminal_view/src/terminal_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3c9836fb75e64d6f7add6e2880bdc409746d4792..9478b3eef14221782050ca9ddf269878a91fef28 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -133,8 +133,8 @@ impl TerminalView { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; - cx.notify(); } + cx.notify(); cx.emit(Event::Wakeup); } Event::Bell => { From 0065f5715ceaa905b5fa6ba07a2ab9ec923498c7 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Thu, 1 Jun 2023 16:40:54 -0400 Subject: [PATCH 041/101] Add panic events --- crates/zed/src/main.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31f331ef93ef17eecb4870f3ed23c9f963a5b3aa..9ec3e3d3f6b5c23c33e0a46ec683ad0bd2f98022 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -41,7 +41,7 @@ use std::{ Arc, Weak, }, thread, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; @@ -376,6 +376,7 @@ struct Panic { backtrace: Vec, // TODO // stripped_backtrace: String, + time: u128, } #[derive(Serialize)] @@ -413,6 +414,10 @@ fn init_panic_hook(app_version: String) { .map(|line| line.to_string()) .collect(), // modified_backtrace: None, + time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), }; if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { From e67e6e6f70b76907d5e44628117a7cc48d0616c9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 1 Jun 2023 14:07:59 -0700 Subject: [PATCH 042/101] Fix ambiguous glob export warnings in rust 1.70 --- crates/editor/src/editor.rs | 4 +++- crates/gpui/src/elements/uniform_list.rs | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index baf2f062fea13633355ee679e12375aaa1b2b6e3..5c19bb0121bdd0302c41c9b067eb72a9eea2e88b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -31,7 +31,9 @@ use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; -pub use element::*; +pub use element::{ + Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, +}; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9ccd57b2919727ec1c60e8af525d8b6de73e8f77..8344914da0d8f0124fd6ddc5cc1cbc29ab2e4d1d 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -36,7 +36,7 @@ struct StateInner { scroll_to: Option, } -pub struct LayoutState { +pub struct UniformListLayoutState { scroll_max: f32, item_height: f32, items: Vec>, @@ -152,7 +152,7 @@ impl UniformList { } impl Element for UniformList { - type LayoutState = LayoutState; + type LayoutState = UniformListLayoutState; type PaintState = (); fn layout( @@ -169,7 +169,7 @@ impl Element for UniformList { let no_items = ( constraint.min, - LayoutState { + UniformListLayoutState { item_height: 0., scroll_max: 0., items: Default::default(), @@ -263,7 +263,7 @@ impl Element for UniformList { ( size, - LayoutState { + UniformListLayoutState { item_height, scroll_max, items, From 585d13d3db5e589174c89fa141493b2d2196076b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 1 Jun 2023 15:20:15 -0700 Subject: [PATCH 043/101] Unzoom all panes when opening a dock --- crates/workspace/src/workspace.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 21feea2b4086433eb1b1e0c31788ce78d16464f4..795747cfcc5045a8c554b9ca8f206b57bb313dc8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1705,7 +1705,7 @@ impl Workspace { ) { // If a center pane is zoomed, unzoom it. for pane in &self.panes { - if pane != &self.active_pane { + if pane != &self.active_pane || dock_to_reveal.is_some() { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); } } @@ -4351,6 +4351,12 @@ mod tests { panel }); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + pane.update(cx, |pane, cx| { + let item = cx.add_view(|_| TestItem::new()); + pane.add_item(Box::new(item), true, true, None, cx); + }); + // Transfer focus from center to panel workspace.update(cx, |workspace, cx| { workspace.toggle_panel_focus::(cx); @@ -4452,6 +4458,25 @@ mod tests { assert!(workspace.zoomed.is_some()); assert!(panel.has_focus(cx)); }); + + // Unzoom and close the panel, zoom the active pane. + panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + + // Opening a dock unzooms the pane. + workspace.update(cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Right, cx) + }); + workspace.read_with(cx, |workspace, cx| { + let pane = pane.read(cx); + assert!(!pane.is_zoomed()); + assert!(pane.has_focus()); + assert!(workspace.right_dock().read(cx).is_open()); + assert!(workspace.zoomed.is_none()); + }); } #[gpui::test] From cc055901e1b530c80a73eb5f98ba1572064c77bd Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Thu, 1 Jun 2023 23:34:39 -0400 Subject: [PATCH 044/101] Add additional panic information to panic events --- crates/feedback/src/system_specs.rs | 2 ++ crates/zed/src/main.rs | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/feedback/src/system_specs.rs b/crates/feedback/src/system_specs.rs index b6eb6e8b6d0f630778398a18b2e2645b807f756b..48955a86291282a3ef4bf55ca27e9a838a2f51e9 100644 --- a/crates/feedback/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -6,6 +6,8 @@ use std::{env, fmt::Display}; use sysinfo::{System, SystemExt}; use util::channel::ReleaseChannel; +// TODO: Move this file out of feedback and into a more general place + #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { #[serde(serialize_with = "serialize_app_version")] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9ec3e3d3f6b5c23c33e0a46ec683ad0bd2f98022..4b0661c6e62f02c05b68172d38c14f886e991136 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -70,10 +70,7 @@ fn main() { log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); - let app_version = ZED_APP_VERSION - .or_else(|| app.platform().app_version().ok()) - .map_or("dev".to_string(), |v| v.to_string()); - init_panic_hook(app_version); + init_panic_hook(&app); app.background(); @@ -376,19 +373,29 @@ struct Panic { backtrace: Vec, // TODO // stripped_backtrace: String, + release_channel: String, + os_name: String, + os_version: Option, + architecture: String, time: u128, } #[derive(Serialize)] struct PanicRequest { panic: Panic, + // TODO: Move to Panic struct, as app_version - requires changing zed.dev version: String, token: String, } -fn init_panic_hook(app_version: String) { +fn init_panic_hook(app: &App) { let is_pty = stdout_is_a_pty(); + let platform = app.platform(); + panic::set_hook(Box::new(move |info| { + let app_version = ZED_APP_VERSION + .or_else(|| platform.app_version().ok()) + .map_or("dev".to_string(), |v| v.to_string()); let backtrace = Backtrace::new(); let thread = thread::current(); @@ -414,6 +421,13 @@ fn init_panic_hook(app_version: String) { .map(|line| line.to_string()) .collect(), // modified_backtrace: None, + release_channel: RELEASE_CHANNEL.dev_name().into(), + os_name: platform.os_name().into(), + os_version: platform + .os_version() + .ok() + .map(|os_version| os_version.to_string()), + architecture: env::consts::ARCH.into(), time: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() From c55aee84d3fcd6d9a4a073168825262dba8d4b73 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 2 Jun 2023 01:33:25 -0400 Subject: [PATCH 045/101] Rename field to panicked_on --- crates/zed/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 4b0661c6e62f02c05b68172d38c14f886e991136..e22edee5f6ce95b6d179af065efff0d1eec9d749 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -377,7 +377,7 @@ struct Panic { os_name: String, os_version: Option, architecture: String, - time: u128, + panicked_on: u128, } #[derive(Serialize)] @@ -428,7 +428,7 @@ fn init_panic_hook(app: &App) { .ok() .map(|os_version| os_version.to_string()), architecture: env::consts::ARCH.into(), - time: SystemTime::now() + panicked_on: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), From cf934ab696ee69b05403e71445a86b2c03992f5e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 10:08:06 +0200 Subject: [PATCH 046/101] Fix compile errors --- assets/keymaps/default.json | 6 +++--- crates/ai/src/assistant.rs | 7 +++---- crates/editor/src/editor.rs | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index f58e3c7cce790124cfaef66816899086182e3633..41087326d27afe9c80a4fc0a27e0b3e31ca8cbf2 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -185,7 +185,8 @@ ], "alt-\\": "copilot::Suggest", "alt-]": "copilot::NextSuggestion", - "alt-[": "copilot::PreviousSuggestion" + "alt-[": "copilot::PreviousSuggestion", + "cmd->": "assistant::QuoteSelection" } }, { @@ -199,8 +200,7 @@ "context": "ContextEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", - "escape": "assistant::CancelLastAssist", - "cmd-?": "assistant::QuoteSelection" + "cmd->": "assistant::QuoteSelection" } }, { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6e76e46c6102cb22de172e0a132d58018883e39c..38b64587a5e1df66fbce33f5ddd86fb27707c397 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,10 +19,7 @@ use workspace::{ pane, Pane, Workspace, }; -actions!( - assistant, - [NewContext, Assist, CancelLastAssist, QuoteSelection] -); +actions!(assistant, [NewContext, Assist, QuoteSelection]); pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); @@ -69,6 +66,7 @@ impl AssistantPanel { .with_child(Pane::render_tab_bar_button( 0, "icons/plus_12.svg", + false, Some(("New Context".into(), Some(Box::new(NewContext)))), cx, move |_, _| todo!(), @@ -81,6 +79,7 @@ impl AssistantPanel { } else { "icons/maximize_8.svg" }, + pane.is_zoomed(), Some(( "Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a3601f3c589877dd44ff997e40e92c2d8e2510c2..ee0644306828b735f8edf3ba5cdaa1a32beffedc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -31,6 +31,7 @@ use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; +pub use element::RenderExcerptHeaderParams; pub use element::{ Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, }; From 55c8c6d3fbf1ba5ad5b4abf9d7e3b34ef2b6e7ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 10:29:22 +0200 Subject: [PATCH 047/101] Allow adding new contexts --- crates/ai/src/ai.rs | 5 ++-- crates/ai/src/assistant.rs | 48 +++++++++++++++++++++++++++----------- crates/zed/src/zed.rs | 7 ++++++ 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 89999f26f39d63591cee4c43274d441439ea5f77..fe1125bb98c848c715b119b77bf6fc7c07e5d9a5 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,10 +1,9 @@ -mod assistant; +pub mod assistant; +pub use assistant::AssistantPanel; use gpui::{actions, AppContext}; use serde::{Deserialize, Serialize}; -pub use assistant::AssistantPanel; - actions!(ai, [Assist]); // Data types for chat completion requests diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 38b64587a5e1df66fbce33f5ddd86fb27707c397..6f5cbd6416ceb5bec5779d76b9cfe8452d4fa01c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,9 +19,18 @@ use workspace::{ pane, Pane, Workspace, }; -actions!(assistant, [NewContext, Assist, QuoteSelection]); +actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]); pub fn init(cx: &mut AppContext) { + cx.add_action( + |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { + if let Some(this) = workspace.panel::(cx) { + this.update(cx, |this, cx| this.add_context(cx)) + } + + workspace.focus_panel::(cx); + }, + ); cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); cx.add_action(AssistantEditor::quote_selection); @@ -49,7 +58,8 @@ impl AssistantPanel { cx.spawn(|mut cx| async move { // TODO: deserialize state. workspace.update(&mut cx, |workspace, cx| { - cx.add_view(|cx| { + cx.add_view::(|cx| { + let weak_self = cx.weak_handle(); let pane = cx.add_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -62,6 +72,7 @@ impl AssistantPanel { pane.set_can_navigate(false, cx); pane.on_can_drop(move |_, _| false); pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + let weak_self = weak_self.clone(); Flex::row() .with_child(Pane::render_tab_bar_button( 0, @@ -69,7 +80,14 @@ impl AssistantPanel { false, Some(("New Context".into(), Some(Box::new(NewContext)))), cx, - move |_, _| todo!(), + move |_, cx| { + let weak_self = weak_self.clone(); + cx.window_context().defer(move |cx| { + if let Some(this) = weak_self.upgrade(cx) { + this.update(cx, |this, cx| this.add_context(cx)); + } + }) + }, None, )) .with_child(Pane::render_tab_bar_button( @@ -125,6 +143,14 @@ impl AssistantPanel { _ => {} } } + + fn add_context(&mut self, cx: &mut ViewContext) { + let focus = self.has_focus(cx); + let editor = cx.add_view(|cx| AssistantEditor::new(self.languages.clone(), cx)); + self.pane.update(cx, |pane, cx| { + pane.add_item(Box::new(editor), true, focus, None, cx) + }); + } } impl Entity for AssistantPanel { @@ -187,11 +213,7 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active && self.pane.read(cx).items_len() == 0 { - let focus = self.has_focus(cx); - let editor = cx.add_view(|cx| AssistantEditor::new(self.languages.clone(), cx)); - self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(editor), true, focus, None, cx) - }); + self.add_context(cx); } } @@ -200,7 +222,7 @@ impl Panel for AssistantPanel { } fn icon_tooltip(&self) -> (String, Option>) { - ("Assistant Panel".into(), None) + ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) } fn should_change_position_on_event(_: &Self::Event) -> bool { @@ -231,7 +253,7 @@ struct Assistant { messages_by_id: HashMap, completion_count: usize, pending_completions: Vec, - language_registry: Arc, + languages: Arc, } impl Entity for Assistant { @@ -246,7 +268,7 @@ impl Assistant { messages_by_id: Default::default(), completion_count: Default::default(), pending_completions: Default::default(), - language_registry, + languages: language_registry, }; this.push_message(Role::User, cx); this @@ -310,7 +332,7 @@ impl Assistant { fn push_message(&mut self, role: Role, cx: &mut ModelContext) -> Message { let content = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); - let markdown = self.language_registry.language_for_name("Markdown"); + let markdown = self.languages.language_for_name("Markdown"); cx.spawn_weak(|buffer, mut cx| async move { let markdown = markdown.await?; let buffer = buffer @@ -322,7 +344,7 @@ impl Assistant { anyhow::Ok(()) }) .detach_and_log_err(cx); - buffer.set_language_registry(self.language_registry.clone()); + buffer.set_language_registry(self.languages.clone()); buffer }); let excerpt_id = self.buffer.update(cx, |buffer, cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 83792514852fe30fb50766e8aca80b1d5e5ae140..38e7a20d310b53176ae116c43b79542fcdfca532 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -235,6 +235,13 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action( + |workspace: &mut Workspace, + _: &ai::assistant::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |_: &NewWindow, cx: &mut AppContext| { From d0aff65b1cb9fe841aed9df8673e50f3deb96dc1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 10:55:19 +0200 Subject: [PATCH 048/101] Allow moving the assistant panel to other docks --- Cargo.lock | 3 + assets/settings/default.json | 10 +++ crates/ai/Cargo.toml | 3 + crates/ai/src/ai.rs | 1 + crates/ai/src/assistant.rs | 98 ++++++++++++++++++++++------- crates/ai/src/assistant_settings.rs | 42 +++++++++++++ crates/terminal_view/Cargo.toml | 2 - 7 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 crates/ai/src/assistant_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 9634cd2c8eb8956e9066b270385035fcc75677a7..4e3ded5bb6b73b2533aea317a03a028f824d62f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,13 +104,16 @@ dependencies = [ "chrono", "collections", "editor", + "fs", "futures 0.3.28", "gpui", "isahc", "language", + "schemars", "search", "serde", "serde_json", + "settings", "theme", "util", "workspace", diff --git a/assets/settings/default.json b/assets/settings/default.json index 23599c8dfb1e327d9d842b76fb8c5e2914c23b3f..695061a0aa07fb42ea6578a2b9e8d0f484a8bffe 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -81,6 +81,16 @@ // Default width of the project panel. "default_width": 240 }, + "assistant": { + // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. + "dock": "right", + // Default width when the assistant is docked to the left or right. + "default_width": 480, + // Default height when the assistant is docked to the bottom. + "default_height": 320, + // OpenAI API key. + "openai_api_key": null + }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 861a9e4785e0fd8373dc6d3ef59d23949a19a939..ce2a3338eb85598da4a6c8ce4a21821dc5eb7afb 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -12,9 +12,11 @@ doctest = false assets = { path = "../assets"} collections = { path = "../collections"} editor = { path = "../editor" } +fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } search = { path = "../search" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } @@ -23,6 +25,7 @@ anyhow.workspace = true chrono = "0.4" futures.workspace = true isahc.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index fe1125bb98c848c715b119b77bf6fc7c07e5d9a5..a5d5666a72fe2cd08431ccbec5cd94e6f603254d 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,4 +1,5 @@ pub mod assistant; +mod assistant_settings; pub use assistant::AssistantPanel; use gpui::{actions, AppContext}; diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6f5cbd6416ceb5bec5779d76b9cfe8452d4fa01c..ecc27538ed1248a895a59b1b7de48f6056f6e3a3 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,8 +1,12 @@ -use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role}; +use crate::{ + assistant_settings::{AssistantDockPosition, AssistantSettings}, + OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, +}; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::HashMap; use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; +use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity, @@ -11,6 +15,7 @@ use gpui::{ }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; +use settings::SettingsStore; use std::{io, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ @@ -22,6 +27,7 @@ use workspace::{ actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]); pub fn init(cx: &mut AppContext) { + settings::register::(cx); cx.add_action( |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { if let Some(this) = workspace.panel::(cx) { @@ -41,12 +47,15 @@ pub enum AssistantPanelEvent { ZoomOut, Focus, Close, + DockPositionChanged, } pub struct AssistantPanel { width: Option, + height: Option, pane: ViewHandle, languages: Arc, + fs: Arc, _subscriptions: Vec, } @@ -113,17 +122,40 @@ impl AssistantPanel { .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); pane }); - let subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - - Self { + let mut this = Self { pane, languages: workspace.app_state().languages.clone(), + fs: workspace.app_state().fs.clone(), width: None, - _subscriptions: subscriptions, - } + height: None, + _subscriptions: Default::default(), + }; + + let mut old_dock_position = this.position(cx); + let mut old_openai_api_key = settings::get::(cx) + .openai_api_key + .clone(); + this._subscriptions = vec![ + cx.observe(&this.pane, |_, _, cx| cx.notify()), + cx.subscribe(&this.pane, Self::handle_pane_event), + cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(AssistantPanelEvent::DockPositionChanged); + } + + let new_openai_api_key = settings::get::(cx) + .openai_api_key + .clone(); + if old_openai_api_key != new_openai_api_key { + old_openai_api_key = new_openai_api_key; + cx.notify(); + } + }), + ]; + + this }) }) }) @@ -174,24 +206,44 @@ impl View for AssistantPanel { } impl Panel for AssistantPanel { - fn position(&self, _: &WindowContext) -> DockPosition { - DockPosition::Right + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } } - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Right) + fn position_is_valid(&self, _: DockPosition) -> bool { + true } - fn set_position(&mut self, _: DockPosition, _: &mut ViewContext) { - // TODO! + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { + let dock = match position { + DockPosition::Left => AssistantDockPosition::Left, + DockPosition::Bottom => AssistantDockPosition::Bottom, + DockPosition::Right => AssistantDockPosition::Right, + }; + settings.dock = Some(dock); + }); } - fn size(&self, _: &WindowContext) -> f32 { - self.width.unwrap_or(480.) + fn size(&self, cx: &WindowContext) -> f32 { + let settings = settings::get::(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or_else(|| settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), + } } fn set_size(&mut self, size: f32, cx: &mut ViewContext) { - self.width = Some(size); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = Some(size), + DockPosition::Bottom => self.height = Some(size), + } cx.notify(); } @@ -225,9 +277,8 @@ impl Panel for AssistantPanel { ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) } - fn should_change_position_on_event(_: &Self::Event) -> bool { - // TODO! - false + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, AssistantPanelEvent::DockPositionChanged) } fn should_activate_on_event(_: &Self::Event) -> bool { @@ -289,7 +340,10 @@ impl Assistant { stream: true, }; - if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { + if let Some(api_key) = settings::get::(cx) + .openai_api_key + .clone() + { let stream = stream_completion(api_key, cx.background().clone(), request); let response = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..c2652ec0cb34e477fe09fe5cba76b6688acaf224 --- /dev/null +++ b/crates/ai/src/assistant_settings.rs @@ -0,0 +1,42 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AssistantDockPosition { + Left, + Right, + Bottom, +} + +#[derive(Deserialize, Debug)] +pub struct AssistantSettings { + pub dock: AssistantDockPosition, + pub default_width: f32, + pub default_height: f32, + pub openai_api_key: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AssistantSettingsContent { + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub openai_api_key: Option, +} + +impl Setting for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 6fa920d739382f4a3f8ccebe8f5b601bce3e4ee0..85de173604db82610a8cf1191771d920fa883583 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -37,8 +37,6 @@ lazy_static.workspace = true serde.workspace = true serde_derive.workspace = true - - [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } From a81d164ea63b659a5f0cd697e9f23c5b0d387e07 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 11:38:02 +0200 Subject: [PATCH 049/101] Allow saving the OpenAI API key in the assistant panel --- Cargo.lock | 1 + assets/settings/default.json | 2 +- crates/ai/Cargo.toml | 1 + crates/ai/src/assistant.rs | 65 +++++++++++++++++++++++++++++-- crates/theme/src/theme.rs | 2 + styles/src/styleTree/assistant.ts | 22 ++++++++++- 6 files changed, 88 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e3ded5bb6b73b2533aea317a03a028f824d62f9..e8b2dfadd4521bcafca6c5ced91233c57e87b019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,7 @@ dependencies = [ "gpui", "isahc", "language", + "menu", "schemars", "search", "serde", diff --git a/assets/settings/default.json b/assets/settings/default.json index 695061a0aa07fb42ea6578a2b9e8d0f484a8bffe..91868fb1e5cdef992a854816a092bafd8ae92b65 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -85,7 +85,7 @@ // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. "dock": "right", // Default width when the assistant is docked to the left or right. - "default_width": 480, + "default_width": 450, // Default height when the assistant is docked to the bottom. "default_height": 320, // OpenAI API key. diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index ce2a3338eb85598da4a6c8ce4a21821dc5eb7afb..9052b1e5edf1fbc1ac300a6d30f556803f4b1dd4 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -15,6 +15,7 @@ editor = { path = "../editor" } fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } +menu = { path = "../menu" } search = { path = "../search" } settings = { path = "../settings" } theme = { path = "../theme" } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index ecc27538ed1248a895a59b1b7de48f6056f6e3a3..1bee432b7bd9f5985e6c3c068af74cbdc6328987 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -40,6 +40,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); cx.add_action(AssistantEditor::quote_selection); + cx.add_action(AssistantPanel::save_api_key); } pub enum AssistantPanelEvent { @@ -54,6 +55,7 @@ pub struct AssistantPanel { width: Option, height: Option, pane: ViewHandle, + api_key_editor: ViewHandle, languages: Arc, fs: Arc, _subscriptions: Vec, @@ -124,6 +126,17 @@ impl AssistantPanel { }); let mut this = Self { pane, + api_key_editor: cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), + cx, + ); + editor.set_placeholder_text( + "sk-000000000000000000000000000000000000000000000000", + cx, + ); + editor + }), languages: workspace.app_state().languages.clone(), fs: workspace.app_state().fs.clone(), width: None, @@ -150,6 +163,9 @@ impl AssistantPanel { .clone(); if old_openai_api_key != new_openai_api_key { old_openai_api_key = new_openai_api_key; + if this.has_focus(cx) { + cx.focus_self(); + } cx.notify(); } }), @@ -183,6 +199,17 @@ impl AssistantPanel { pane.add_item(Box::new(editor), true, focus, None, cx) }); } + + fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + let api_key = self.api_key_editor.read(cx).text(cx); + if !api_key.is_empty() { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| settings.openai_api_key = Some(api_key), + ); + } + } } impl Entity for AssistantPanel { @@ -195,12 +222,44 @@ impl View for AssistantPanel { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.pane, cx).into_any() + let style = &theme::current(cx).assistant; + if settings::get::(cx) + .openai_api_key + .is_none() + { + Flex::column() + .with_child( + Text::new( + "Paste your OpenAI API key and press Enter to use the assistant", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) + .with_child( + ChildView::new(&self.api_key_editor, cx) + .contained() + .with_style(style.api_key_editor.container) + .aligned(), + ) + .contained() + .with_style(style.api_key_prompt.container) + .aligned() + .into_any() + } else { + ChildView::new(&self.pane, cx).into_any() + } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { - cx.focus(&self.pane); + if settings::get::(cx) + .openai_api_key + .is_some() + { + cx.focus(&self.pane); + } else { + cx.focus(&self.api_key_editor); + } } } } @@ -290,7 +349,7 @@ impl Panel for AssistantPanel { } fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() + self.pane.read(cx).has_focus() || self.api_key_editor.is_focused(cx) } fn is_focus_event(event: &Self::Event) -> bool { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f1e752b763e07f3449099b85d45961d80bca7dd8..8282336ba554e340e09b7fa15ce5a049bbd4af89 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -976,6 +976,8 @@ pub struct AssistantStyle { pub sent_at: ContainedText, pub user_sender: ContainedText, pub assistant_sender: ContainedText, + pub api_key_editor: FieldEditor, + pub api_key_prompt: ContainedText, } #[derive(Clone, Deserialize, Default)] diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 0ff65a22ae3a730e0152bf4995cd26e6599aea57..085e43071c12e6896d229bff3ff8dd4ababff874 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { text, border } from "./components" +import { text, border, background } from "./components" import editor from "./editor" export default function assistant(colorScheme: ColorScheme) { @@ -22,6 +22,26 @@ export default function assistant(colorScheme: ColorScheme) { sent_at: { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), + }, + apiKeyEditor: { + background: background(layer, "on"), + cornerRadius: 6, + text: text(layer, "mono", "on"), + placeholderText: text(layer, "mono", "on", "disabled", { + size: "xs", + }), + selection: colorScheme.players[0], + border: border(layer, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + apiKeyPrompt: { + padding: 10, + ...text(layer, "sans", "default", { size: "xs" }), } } } From 3750e64d9f4e46e24670bb41704bebd2ea625e7d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 12:15:25 +0200 Subject: [PATCH 050/101] Save OpenAI API key in the keychain --- assets/settings/default.json | 4 +- crates/ai/src/assistant.rs | 157 ++++++++++++++++++---------- crates/ai/src/assistant_settings.rs | 2 - 3 files changed, 100 insertions(+), 63 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 91868fb1e5cdef992a854816a092bafd8ae92b65..e6f006bf60d79d34835ce063889ea68eeecdb562 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -87,9 +87,7 @@ // Default width when the assistant is docked to the left or right. "default_width": 450, // Default height when the assistant is docked to the bottom. - "default_height": 320, - // OpenAI API key. - "openai_api_key": null + "default_height": 320 }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1bee432b7bd9f5985e6c3c068af74cbdc6328987..39e9a6ba157bf0501fb3d3e65b123874175b91c0 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -16,7 +16,7 @@ use gpui::{ use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; -use std::{io, sync::Arc}; +use std::{cell::Cell, io, rc::Rc, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -24,7 +24,12 @@ use workspace::{ pane, Pane, Workspace, }; -actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]); +const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + +actions!( + assistant, + [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey] +); pub fn init(cx: &mut AppContext) { settings::register::(cx); @@ -41,6 +46,7 @@ pub fn init(cx: &mut AppContext) { cx.capture_action(AssistantEditor::cancel_last_assist); cx.add_action(AssistantEditor::quote_selection); cx.add_action(AssistantPanel::save_api_key); + cx.add_action(AssistantPanel::reset_api_key); } pub enum AssistantPanelEvent { @@ -55,7 +61,9 @@ pub struct AssistantPanel { width: Option, height: Option, pane: ViewHandle, - api_key_editor: ViewHandle, + api_key: Rc>>, + api_key_editor: Option>, + has_read_credentials: bool, languages: Arc, fs: Arc, _subscriptions: Vec, @@ -124,19 +132,12 @@ impl AssistantPanel { .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); pane }); + let mut this = Self { pane, - api_key_editor: cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), - cx, - ); - editor.set_placeholder_text( - "sk-000000000000000000000000000000000000000000000000", - cx, - ); - editor - }), + api_key: Rc::new(Cell::new(None)), + api_key_editor: None, + has_read_credentials: false, languages: workspace.app_state().languages.clone(), fs: workspace.app_state().fs.clone(), width: None, @@ -145,9 +146,6 @@ impl AssistantPanel { }; let mut old_dock_position = this.position(cx); - let mut old_openai_api_key = settings::get::(cx) - .openai_api_key - .clone(); this._subscriptions = vec![ cx.observe(&this.pane, |_, _, cx| cx.notify()), cx.subscribe(&this.pane, Self::handle_pane_event), @@ -157,17 +155,6 @@ impl AssistantPanel { old_dock_position = new_dock_position; cx.emit(AssistantPanelEvent::DockPositionChanged); } - - let new_openai_api_key = settings::get::(cx) - .openai_api_key - .clone(); - if old_openai_api_key != new_openai_api_key { - old_openai_api_key = new_openai_api_key; - if this.has_focus(cx) { - cx.focus_self(); - } - cx.notify(); - } }), ]; @@ -194,22 +181,49 @@ impl AssistantPanel { fn add_context(&mut self, cx: &mut ViewContext) { let focus = self.has_focus(cx); - let editor = cx.add_view(|cx| AssistantEditor::new(self.languages.clone(), cx)); + let editor = cx + .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(editor), true, focus, None, cx) }); } fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - let api_key = self.api_key_editor.read(cx).text(cx); - if !api_key.is_empty() { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings| settings.openai_api_key = Some(api_key), - ); + if let Some(api_key) = self + .api_key_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + { + if !api_key.is_empty() { + cx.platform() + .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) + .log_err(); + self.api_key.set(Some(api_key)); + self.api_key_editor.take(); + cx.focus_self(); + cx.notify(); + } } } + + fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext) { + cx.platform().delete_credentials(OPENAI_API_URL).log_err(); + self.api_key.take(); + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.focus_self(); + cx.notify(); + } +} + +fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { + cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), + cx, + ); + editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); + editor + }) } impl Entity for AssistantPanel { @@ -223,10 +237,7 @@ impl View for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let style = &theme::current(cx).assistant; - if settings::get::(cx) - .openai_api_key - .is_none() - { + if let Some(api_key_editor) = self.api_key_editor.as_ref() { Flex::column() .with_child( Text::new( @@ -236,7 +247,7 @@ impl View for AssistantPanel { .aligned(), ) .with_child( - ChildView::new(&self.api_key_editor, cx) + ChildView::new(api_key_editor, cx) .contained() .with_style(style.api_key_editor.container) .aligned(), @@ -252,13 +263,10 @@ impl View for AssistantPanel { fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { - if settings::get::(cx) - .openai_api_key - .is_some() - { - cx.focus(&self.pane); + if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus(api_key_editor); } else { - cx.focus(&self.api_key_editor); + cx.focus(&self.pane); } } } @@ -323,8 +331,30 @@ impl Panel for AssistantPanel { } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - if active && self.pane.read(cx).items_len() == 0 { - self.add_context(cx); + if active { + if self.api_key.clone().take().is_none() && !self.has_read_credentials { + self.has_read_credentials = true; + let api_key = if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + if let Some(api_key) = api_key { + self.api_key.set(Some(api_key)); + } else if self.api_key_editor.is_none() { + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.notify(); + } + } + + if self.pane.read(cx).items_len() == 0 { + self.add_context(cx); + } } } @@ -349,7 +379,11 @@ impl Panel for AssistantPanel { } fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() || self.api_key_editor.is_focused(cx) + self.pane.read(cx).has_focus() + || self + .api_key_editor + .as_ref() + .map_or(false, |editor| editor.is_focused(cx)) } fn is_focus_event(event: &Self::Event) -> bool { @@ -364,6 +398,7 @@ struct Assistant { completion_count: usize, pending_completions: Vec, languages: Arc, + api_key: Rc>>, } impl Entity for Assistant { @@ -371,7 +406,11 @@ impl Entity for Assistant { } impl Assistant { - fn new(language_registry: Arc, cx: &mut ModelContext) -> Self { + fn new( + api_key: Rc>>, + language_registry: Arc, + cx: &mut ModelContext, + ) -> Self { let mut this = Self { buffer: cx.add_model(|_| MultiBuffer::new(0)), messages: Default::default(), @@ -379,6 +418,7 @@ impl Assistant { completion_count: Default::default(), pending_completions: Default::default(), languages: language_registry, + api_key, }; this.push_message(Role::User, cx); this @@ -399,10 +439,7 @@ impl Assistant { stream: true, }; - if let Some(api_key) = settings::get::(cx) - .openai_api_key - .clone() - { + if let Some(api_key) = self.api_key.clone().take() { let stream = stream_completion(api_key, cx.background().clone(), request); let response = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); @@ -496,8 +533,12 @@ struct AssistantEditor { } impl AssistantEditor { - fn new(language_registry: Arc, cx: &mut ViewContext) -> Self { - let assistant = cx.add_model(|cx| Assistant::new(language_registry, cx)); + fn new( + api_key: Rc>>, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx)); let editor = cx.add_view(|cx| { let mut editor = Editor::for_multibuffer(assistant.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); @@ -685,7 +726,7 @@ async fn stream_completion( let (tx, rx) = futures::channel::mpsc::unbounded::>(); let json_data = serde_json::to_string(&request)?; - let mut response = Request::post("https://api.openai.com/v1/chat/completions") + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {}", api_key)) .body(json_data)? diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs index c2652ec0cb34e477fe09fe5cba76b6688acaf224..eb92e0f6e8c0bdd6e554844f2565057ed92e9ebd 100644 --- a/crates/ai/src/assistant_settings.rs +++ b/crates/ai/src/assistant_settings.rs @@ -16,7 +16,6 @@ pub struct AssistantSettings { pub dock: AssistantDockPosition, pub default_width: f32, pub default_height: f32, - pub openai_api_key: Option, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -24,7 +23,6 @@ pub struct AssistantSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, - pub openai_api_key: Option, } impl Setting for AssistantSettings { From f00f16fe3733640321ed5b31f8e744897cd0ecf2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 17:21:18 +0200 Subject: [PATCH 051/101] Show remaining tokens --- Cargo.lock | 43 ++++++++++++ crates/ai/Cargo.toml | 1 + crates/ai/src/assistant.rs | 108 ++++++++++++++++++++++++++++-- crates/editor/src/editor.rs | 2 +- crates/theme/src/theme.rs | 2 + styles/src/styleTree/assistant.ts | 14 ++++ 6 files changed, 162 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8b2dfadd4521bcafca6c5ced91233c57e87b019..9c4628e9fb70500d8e3611c7f9b6cbf7a3289f79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,7 @@ dependencies = [ "serde_json", "settings", "theme", + "tiktoken-rs", "util", "workspace", ] @@ -745,6 +746,21 @@ dependencies = [ "which", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -870,6 +886,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", + "once_cell", + "regex-automata", "serde", ] @@ -2220,6 +2238,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -6969,6 +6997,21 @@ dependencies = [ "weezl", ] +[[package]] +name = "tiktoken-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4" +dependencies = [ + "anyhow", + "base64 0.21.0", + "bstr", + "fancy-regex", + "lazy_static", + "parking_lot 0.12.1", + "rustc-hash", +] + [[package]] name = "time" version = "0.1.45" diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 9052b1e5edf1fbc1ac300a6d30f556803f4b1dd4..e36df880d982f1e7ff267184b9c5a1877ea9772c 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -29,6 +29,7 @@ isahc.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +tiktoken-rs = "0.4" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 39e9a6ba157bf0501fb3d3e65b123874175b91c0..68f722f1eef128ce4d0bdb4b51f5157e5921d713 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -16,7 +16,8 @@ use gpui::{ use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; -use std::{cell::Cell, io, rc::Rc, sync::Arc}; +use std::{cell::Cell, io, rc::Rc, sync::Arc, time::Duration}; +use tiktoken_rs::model::get_context_size; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -398,7 +399,12 @@ struct Assistant { completion_count: usize, pending_completions: Vec, languages: Arc, + model: String, + token_count: Option, + max_token_count: usize, + pending_token_count: Task>, api_key: Rc>>, + _subscriptions: Vec, } impl Entity for Assistant { @@ -411,19 +417,78 @@ impl Assistant { language_registry: Arc, cx: &mut ModelContext, ) -> Self { + let model = "gpt-3.5-turbo"; + let buffer = cx.add_model(|_| MultiBuffer::new(0)); let mut this = Self { - buffer: cx.add_model(|_| MultiBuffer::new(0)), messages: Default::default(), messages_by_id: Default::default(), completion_count: Default::default(), pending_completions: Default::default(), languages: language_registry, + token_count: None, + max_token_count: get_context_size(model), + pending_token_count: Task::ready(None), + model: model.into(), + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], api_key, + buffer, }; this.push_message(Role::User, cx); + this.count_remaining_tokens(cx); this } + fn handle_buffer_event( + &mut self, + _: ModelHandle, + event: &editor::multi_buffer::Event, + cx: &mut ModelContext, + ) { + match event { + editor::multi_buffer::Event::ExcerptsAdded { .. } + | editor::multi_buffer::Event::ExcerptsRemoved { .. } + | editor::multi_buffer::Event::Edited => self.count_remaining_tokens(cx), + _ => {} + } + } + + fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + let messages = self + .messages + .iter() + .map(|message| tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: message.content.read(cx).text(), + name: None, + }) + .collect::>(); + let model = self.model.clone(); + self.pending_token_count = cx.spawn(|this, mut cx| { + async move { + cx.background().timer(Duration::from_millis(200)).await; + let token_count = cx + .background() + .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) }) + .await?; + + this.update(&mut cx, |this, cx| { + this.token_count = Some(token_count); + cx.notify() + }); + anyhow::Ok(()) + } + .log_err() + }); + } + + fn remaining_tokens(&self) -> Option { + Some(self.max_token_count as isize - self.token_count? as isize) + } + fn assist(&mut self, cx: &mut ModelContext) { let messages = self .messages @@ -434,7 +499,7 @@ impl Assistant { }) .collect(); let request = OpenAIRequest { - model: "gpt-3.5-turbo".into(), + model: self.model.clone(), messages, stream: true, }; @@ -530,6 +595,7 @@ struct PendingCompletion { struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, + _subscriptions: Vec, } impl AssistantEditor { @@ -590,7 +656,11 @@ impl AssistantEditor { ); editor }); - Self { assistant, editor } + Self { + _subscriptions: vec![cx.observe(&assistant, |_, _, cx| cx.notify())], + assistant, + editor, + } } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { @@ -684,10 +754,34 @@ impl View for AssistantEditor { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = &theme::current(cx).assistant; + let remaining_tokens = self + .assistant + .read(cx) + .remaining_tokens() + .map(|remaining_tokens| { + let remaining_tokens_style = if remaining_tokens <= 0 { + &theme.no_remaining_tokens + } else { + &theme.remaining_tokens + }; + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container) + .aligned() + .top() + .right() + }); - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container) + Stack::new() + .with_child( + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container), + ) + .with_children(remaining_tokens) .into_any() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ee0644306828b735f8edf3ba5cdaa1a32beffedc..453468349be90a2b8f6165e6710a1ba23460d9ea 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,7 +10,7 @@ pub mod items; mod link_go_to_definition; mod mouse_context_menu; pub mod movement; -mod multi_buffer; +pub mod multi_buffer; mod persistence; pub mod scroll; pub mod selections_collection; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 8282336ba554e340e09b7fa15ce5a049bbd4af89..97aac92afd153d7c7bb4267ac8e5ca445ada7a10 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -976,6 +976,8 @@ pub struct AssistantStyle { pub sent_at: ContainedText, pub user_sender: ContainedText, pub assistant_sender: ContainedText, + pub remaining_tokens: ContainedText, + pub no_remaining_tokens: ContainedText, pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, } diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 085e43071c12e6896d229bff3ff8dd4ababff874..3d21ee8519f3eeb30e8437e07f69eca5f72c8597 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -23,6 +23,20 @@ export default function assistant(colorScheme: ColorScheme) { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), }, + remaining_tokens: { + padding: 4, + margin: { right: 16, top: 4 }, + background: background(layer, "on"), + cornerRadius: 4, + ...text(layer, "sans", "positive", { size: "xs" }), + }, + no_remaining_tokens: { + padding: 4, + margin: { right: 16, top: 4 }, + background: background(layer, "on"), + cornerRadius: 4, + ...text(layer, "sans", "negative", { size: "xs" }), + }, apiKeyEditor: { background: background(layer, "on"), cornerRadius: 6, From 345fad3e9d522d0e1c8c91674606ca8c5e20de37 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 2 Jun 2023 17:32:34 +0200 Subject: [PATCH 052/101] editor: add select previous command (#2556) Added a `select previous` command to complement `select next`. Release Notes: - Added "Select previous" editor command, mirroring `Select next`. Ticket number: Z-366 --- assets/keymaps/atom.json | 6 ++ assets/keymaps/default.json | 12 ++++ assets/keymaps/jetbrains.json | 6 ++ crates/editor/src/editor.rs | 110 ++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 51 ++++++++++++++ crates/editor/src/multi_buffer.rs | 87 ++++++++++++++++++++++- crates/rope/src/rope.rs | 48 ++++++++++--- crates/text/src/text.rs | 6 ++ 8 files changed, 316 insertions(+), 10 deletions(-) diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 634aed322abde3333cef56f352a752d03e68005f..25143914cc4828d14e36af9e4b77b5a41cc87d4b 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -16,6 +16,12 @@ "replace_newest": true } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-up": "editor::AddSelectionAbove", "cmd-shift-backspace": "editor::DeleteToBeginningOfLine", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7e1a8429bfd695a75b423b5e2a175d474ad3d49b..46a3fb5a5a7f63d5855d8fb2f8ff9023b82c9dac 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -250,12 +250,24 @@ "replace_newest": false } ], + "ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-k cmd-d": [ "editor::SelectNext", { "replace_newest": true } ], + "cmd-k ctrl-cmd-d": [ + "editor::SelectPrevious", + { + "replace_newest": true + } + ], "cmd-k cmd-i": "editor::Hover", "cmd-/": [ "editor::ToggleComments", diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 4825d3e8b5fa2163e5a40b446c390f8d18ef96e8..b3e8f989a4a0337a3c1dd9dff63ef640d64dc6ef 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -26,6 +26,12 @@ "replace_newest": false } ], + "ctrl-cmd-g": [ + "editor::SelectPrevious", + { + "replace_newest": false + } + ], "cmd-/": [ "editor::ToggleComments", { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5c19bb0121bdd0302c41c9b067eb72a9eea2e88b..af7344a91f230b6ace019a179ea242011958c597 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,6 +111,12 @@ pub struct SelectNext { pub replace_newest: bool, } +#[derive(Clone, Deserialize, PartialEq, Default)] +pub struct SelectPrevious { + #[serde(default)] + pub replace_newest: bool, +} + #[derive(Clone, Deserialize, PartialEq)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -272,6 +278,7 @@ impl_actions!( editor, [ SelectNext, + SelectPrevious, SelectToBeginningOfLine, SelectToEndOfLine, ToggleCodeActions, @@ -367,6 +374,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::add_selection_above); cx.add_action(Editor::add_selection_below); cx.add_action(Editor::select_next); + cx.add_action(Editor::select_previous); cx.add_action(Editor::toggle_comments); cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); @@ -484,6 +492,7 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, + select_prev_state: Option, selection_history: SelectionHistory, autoclose_regions: Vec, snippet_stack: InvalidationStack, @@ -539,6 +548,7 @@ pub struct EditorSnapshot { struct SelectionHistoryEntry { selections: Arc<[Selection]>, select_next_state: Option, + select_prev_state: Option, add_selections_state: Option, } @@ -1286,6 +1296,7 @@ impl Editor { columnar_selection_tail: None, add_selections_state: None, select_next_state: None, + select_prev_state: None, selection_history: Default::default(), autoclose_regions: Default::default(), snippet_stack: Default::default(), @@ -1507,6 +1518,7 @@ impl Editor { let buffer = &display_map.buffer_snapshot; self.add_selections_state = None; self.select_next_state = None; + self.select_prev_state = None; self.select_larger_syntax_node_stack.clear(); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack @@ -5213,6 +5225,101 @@ impl Editor { } } + pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext) { + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges([next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + s.insert_range(next_selected_range); + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: true, + done: false, + }; + self.unfold_ranges([selection.start..selection.end], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select(selections); + }); + self.select_prev_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let query = query.chars().rev().collect::(); + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: false, + done: false, + }); + self.select_previous(action, cx); + } + } + } + pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); @@ -5586,6 +5693,7 @@ impl Editor { if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -5598,6 +5706,7 @@ impl Editor { if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::newest(), cx); } @@ -6375,6 +6484,7 @@ impl Editor { self.selection_history.push(SelectionHistoryEntry { selections: self.selections.disjoint_anchors(), select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), add_selections_state: self.add_selections_state.clone(), }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bc671b9ffc3652856d16faf567784eab438440b7..969b9c882d937a165473e06715b29f6352ed4fbc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3107,6 +3107,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); } +#[gpui::test] +async fn test_select_previous(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + { + // `Select previous` without a selection (selects wordwise) + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\nˇabc abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); + } + { + // `Select previous` with a selection + let mut cx = EditorTestContext::new(cx).await; + cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); + + cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); + + cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)); + cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); + } +} + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b7b2b89c8ca312667bfb3b5988da8292fd33c6bc..af0003ec2dde8f6fe302c9fb7c0aabfdcf91c2ff 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -196,6 +196,13 @@ pub struct MultiBufferBytes<'a> { chunk: &'a [u8], } +pub struct ReversedMultiBufferBytes<'a> { + range: Range, + excerpts: Cursor<'a, Excerpt, usize>, + excerpt_bytes: Option>, + chunk: &'a [u8], +} + struct ExcerptChunks<'a> { content_chunks: BufferChunks<'a>, footer_height: usize, @@ -1967,7 +1974,6 @@ impl MultiBufferSnapshot { } else { None }; - MultiBufferBytes { range, excerpts, @@ -1976,6 +1982,33 @@ impl MultiBufferSnapshot { } } + pub fn reversed_bytes_in_range( + &self, + range: Range, + ) -> ReversedMultiBufferBytes { + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut excerpts = self.excerpts.cursor::(); + excerpts.seek(&range.end, Bias::Left, &()); + + let mut chunk = &[][..]; + let excerpt_bytes = if let Some(excerpt) = excerpts.item() { + let mut excerpt_bytes = excerpt.reversed_bytes_in_range( + range.start - excerpts.start()..range.end - excerpts.start(), + ); + chunk = excerpt_bytes.next().unwrap_or(&[][..]); + Some(excerpt_bytes) + } else { + None + }; + + ReversedMultiBufferBytes { + range, + excerpts, + excerpt_bytes, + chunk, + } + } + pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { let mut result = MultiBufferRows { buffer_row_range: 0..0, @@ -3409,6 +3442,26 @@ impl Excerpt { } } + fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { + let content_start = self.range.context.start.to_offset(&self.buffer); + let bytes_start = content_start + range.start; + let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); + let footer_height = if self.has_trailing_newline + && range.start <= self.text_summary.len + && range.end > self.text_summary.len + { + 1 + } else { + 0 + }; + let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); + + ExcerptBytes { + content_bytes, + footer_height, + } + } + fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { if text_anchor .cmp(&self.range.context.start, &self.buffer) @@ -3727,6 +3780,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> { } } +impl<'a> ReversedMultiBufferBytes<'a> { + fn consume(&mut self, len: usize) { + self.range.end -= len; + self.chunk = &self.chunk[..self.chunk.len() - len]; + + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { + self.chunk = chunk; + } else { + self.excerpts.next(&()); + if let Some(excerpt) = self.excerpts.item() { + let mut excerpt_bytes = + excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); + self.chunk = excerpt_bytes.next().unwrap(); + self.excerpt_bytes = Some(excerpt_bytes); + } + } + } + } +} + +impl<'a> io::Read for ReversedMultiBufferBytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let len = cmp::min(buf.len(), self.chunk.len()); + buf[..len].copy_from_slice(&self.chunk[..len]); + buf[..len].reverse(); + if len > 0 { + self.consume(len); + } + Ok(len) + } +} impl<'a> Iterator for ExcerptBytes<'a> { type Item = &'a [u8]; diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 797fb39317bd42596063406a85d4ee6f2d7bf37c..6b6f364fdb0b7395be9db6d516a46f1b40d042bd 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -179,7 +179,11 @@ impl Rope { } pub fn bytes_in_range(&self, range: Range) -> Bytes { - Bytes::new(self, range) + Bytes::new(self, range, false) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, true) } pub fn chunks(&self) -> Chunks { @@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> { pub struct Bytes<'a> { chunks: sum_tree::Cursor<'a, Chunk, usize>, range: Range, + reversed: bool, } impl<'a> Bytes<'a> { - pub fn new(rope: &'a Rope, range: Range) -> Self { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(); - chunks.seek(&range.start, Bias::Right, &()); - Self { chunks, range } + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } } pub fn peek(&self) -> Option<&'a [u8]> { let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } let chunk_start = *self.chunks.start(); if self.range.end <= chunk_start { return None; } - let start = self.range.start.saturating_sub(chunk_start); let end = self.range.end - chunk_start; Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) @@ -607,7 +622,11 @@ impl<'a> Iterator for Bytes<'a> { fn next(&mut self) -> Option { let result = self.peek(); if result.is_some() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } result } @@ -617,10 +636,21 @@ impl<'a> io::Read for Bytes<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if let Some(chunk) = self.peek() { let len = cmp::min(buf.len(), chunk.len()); - buf[..len].copy_from_slice(&chunk[..len]); - self.range.start += len; + if self.reversed { + buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); + buf[..len].reverse(); + self.range.end -= len; + } else { + buf[..len].copy_from_slice(&chunk[..len]); + self.range.start += len; + } + if len == chunk.len() { - self.chunks.next(&()); + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } } Ok(len) } else { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index dcfaf818d1f97f98002d60fecf90533d50c2969b..2693add8ed0ac0d9d032ad5bd3f5e540138cf2fe 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1749,6 +1749,12 @@ impl BufferSnapshot { self.visible_text.bytes_in_range(start..end) } + pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { + let start = range.start.to_offset(self); + let end = range.end.to_offset(self); + self.visible_text.reversed_bytes_in_range(start..end) + } + pub fn text_for_range(&self, range: Range) -> Chunks<'_> { let start = range.start.to_offset(self); let end = range.end.to_offset(self); From f6a4706410ed9d547cd3757c41ee63e76a97bce3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 2 Jun 2023 12:32:36 -0700 Subject: [PATCH 053/101] Improve panic reports * Add an 'identifying_backtrace' field that only contains symbols in our codebase, which can be used for better deduplication. * In the main backtrace, include file and line numbers for all symbols in our codebase --- crates/zed/src/main.rs | 85 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e22edee5f6ce95b6d179af065efff0d1eec9d749..6248ccf4f0bf888028a7dae775f7e7e0bcbefdae 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -32,6 +32,7 @@ use std::{ ffi::OsStr, fs::OpenOptions, io::Write as _, + ops::Not, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -371,13 +372,12 @@ struct Panic { #[serde(skip_serializing_if = "Option::is_none")] location_data: Option, backtrace: Vec, - // TODO - // stripped_backtrace: String, release_channel: String, os_name: String, os_version: Option, architecture: String, panicked_on: u128, + identifying_backtrace: Option>, } #[derive(Serialize)] @@ -396,18 +396,73 @@ fn init_panic_hook(app: &App) { let app_version = ZED_APP_VERSION .or_else(|| platform.app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); - let backtrace = Backtrace::new(); let thread = thread::current(); let thread = thread.name().unwrap_or(""); - let payload = match info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match info.payload().downcast_ref::() { - Some(s) => &**s, - None => "Box", - }, - }; + let payload = info.payload(); + let payload = None + .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string())) + .or_else(|| payload.downcast_ref::().map(|s| s.clone())) + .unwrap_or_else(|| "Box".to_string()); + + let backtrace = Backtrace::new(); + let backtrace = backtrace + .frames() + .iter() + .filter_map(|frame| { + let symbol = frame.symbols().first()?; + let path = symbol.filename()?; + Some((path, symbol.lineno(), format!("{:#}", symbol.name()?))) + }) + .collect::>(); + + let this_file_path = Path::new(file!()); + + // Find the first frame in the backtrace for this panic hook itself. Exclude + // that frame and all frames before it. + let mut start_frame_ix = 0; + let mut codebase_root_path = None; + for (ix, (path, _, _)) in backtrace.iter().enumerate() { + if path.ends_with(this_file_path) { + start_frame_ix = ix + 1; + codebase_root_path = path.ancestors().nth(this_file_path.components().count()); + break; + } + } + + // Exclude any subsequent frames inside of rust's panic handling system. + while let Some((path, _, _)) = backtrace.get(start_frame_ix) { + if path.starts_with("/rustc") { + start_frame_ix += 1; + } else { + break; + } + } + + // Build two backtraces: + // * one for display, which includes symbol names for all frames, and files + // and line numbers for symbols in this codebase + // * one for identification and de-duplication, which only includes symbol + // names for symbols in this codebase. + let mut display_backtrace = Vec::new(); + let mut identifying_backtrace = Vec::new(); + for (path, line, symbol) in &backtrace[start_frame_ix..] { + display_backtrace.push(symbol.clone()); + + if let Some(codebase_root_path) = &codebase_root_path { + if let Ok(suffix) = path.strip_prefix(&codebase_root_path) { + identifying_backtrace.push(symbol.clone()); + + let display_path = suffix.to_string_lossy(); + if let Some(line) = line { + display_backtrace.push(format!(" {display_path}:{line}")); + } else { + display_backtrace.push(format!(" {display_path}")); + } + } + } + } let panic_data = Panic { thread: thread.into(), @@ -416,11 +471,6 @@ fn init_panic_hook(app: &App) { file: location.file().into(), line: location.line(), }), - backtrace: format!("{:?}", backtrace) - .split("\n") - .map(|line| line.to_string()) - .collect(), - // modified_backtrace: None, release_channel: RELEASE_CHANNEL.dev_name().into(), os_name: platform.os_name().into(), os_version: platform @@ -432,6 +482,11 @@ fn init_panic_hook(app: &App) { .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), + backtrace: display_backtrace, + identifying_backtrace: identifying_backtrace + .is_empty() + .not() + .then_some(identifying_backtrace), }; if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { From 7417835f06a851ad1c6a6416727dbd06f15a1bf5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 2 Jun 2023 13:02:17 -0700 Subject: [PATCH 054/101] Avoid writing spurious nulls to settings file when updating it programatically --- crates/settings/src/settings_store.rs | 62 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 3fdb81dd45cfee2671314ecc902cb38958841579..1133cb597067e70472ec6267ffb9575751efe36a 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -623,22 +623,6 @@ impl AnySettingValue for SettingValue { } } -// impl Debug for SettingsStore { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// return f -// .debug_struct("SettingsStore") -// .field( -// "setting_value_sets_by_type", -// &self -// .setting_values -// .values() -// .map(|set| (set.setting_type_name(), set)) -// .collect::>(), -// ) -// .finish_non_exhaustive(); -// } -// } - fn update_value_in_json_text<'a>( text: &mut String, key_path: &mut Vec<&'a str>, @@ -681,6 +665,10 @@ fn update_value_in_json_text<'a>( key_path.pop(); } } else if old_value != new_value { + let mut new_value = new_value.clone(); + if let Some(new_object) = new_value.as_object_mut() { + new_object.retain(|_, v| !v.is_null()); + } let (range, replacement) = replace_value_in_json_text(text, &key_path, tab_size, &new_value); text.replace_range(range.clone(), &replacement); @@ -692,7 +680,7 @@ fn replace_value_in_json_text( text: &str, key_path: &[&str], tab_size: usize, - new_value: impl Serialize, + new_value: &serde_json::Value, ) -> (Range, String) { const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; const LANGUAGES: &'static str = "languages"; @@ -1039,24 +1027,32 @@ mod tests { r#"{ "languages": { "JSON": { - "is_enabled": true + "language_setting_1": true } } }"# .unindent(), |settings| { - settings.languages.get_mut("JSON").unwrap().is_enabled = false; settings .languages - .insert("Rust".into(), LanguageSettingEntry { is_enabled: true }); + .get_mut("JSON") + .unwrap() + .language_setting_1 = Some(false); + settings.languages.insert( + "Rust".into(), + LanguageSettingEntry { + language_setting_2: Some(true), + ..Default::default() + }, + ); }, r#"{ "languages": { "Rust": { - "is_enabled": true + "language_setting_2": true }, "JSON": { - "is_enabled": false + "language_setting_1": false } } }"# @@ -1119,6 +1115,23 @@ mod tests { .unindent(), cx, ); + + check_settings_update::( + &mut store, + r#"{ + } + "# + .unindent(), + |settings| settings.age = Some(37), + r#"{ + "user": { + "age": 37 + } + } + "# + .unindent(), + cx, + ); } fn check_settings_update( @@ -1247,9 +1260,10 @@ mod tests { languages: HashMap, } - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] struct LanguageSettingEntry { - is_enabled: bool, + language_setting_1: Option, + language_setting_2: Option, } impl Setting for LanguageSettings { From 7c60f636d5d0d947137e750461219e15a588c655 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Fri, 2 Jun 2023 22:02:19 -0400 Subject: [PATCH 055/101] Fix typos --- assets/keymaps/default.json | 2 +- assets/settings/default.json | 2 +- crates/cli/src/main.rs | 5 +---- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- crates/copilot/src/copilot.rs | 2 +- crates/editor/src/display_map.rs | 4 ++-- crates/editor/src/editor.rs | 6 +++--- crates/editor/src/editor_tests.rs | 6 +++--- crates/editor/src/element.rs | 2 +- crates/editor/src/multi_buffer.rs | 2 +- crates/editor/src/test.rs | 4 ++-- crates/fs/src/fs.rs | 6 +++--- crates/gpui/src/app.rs | 4 ++-- crates/gpui/src/keymap_matcher.rs | 2 +- crates/language/src/language_settings.rs | 2 +- crates/language/src/syntax_map.rs | 4 ++-- crates/plugin_macros/src/lib.rs | 4 ++-- crates/plugin_runtime/OPAQUE.md | 4 ++-- crates/plugin_runtime/src/plugin.rs | 10 +++++----- crates/project/src/project.rs | 2 +- crates/project/src/worktree.rs | 4 ++-- crates/project_symbols/src/project_symbols.rs | 2 +- crates/rpc/proto/zed.proto | 2 +- crates/rpc/src/auth.rs | 2 +- crates/sqlez/src/thread_safe_connection.rs | 8 ++++---- crates/terminal/src/mappings/colors.rs | 4 ++-- crates/terminal_view/README.md | 2 +- crates/terminal_view/scripts/print256color.sh | 2 +- crates/terminal_view/src/terminal_element.rs | 4 ++-- crates/terminal_view/src/terminal_view.rs | 4 ++-- crates/text/src/tests.rs | 4 ++-- crates/vim/src/normal.rs | 2 +- crates/vim/src/test/neovim_connection.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/languages/typescript.rs | 2 +- crates/zed/src/main.rs | 2 +- styles/src/buildLicenses.ts | 2 +- styles/src/styleTree/editor.ts | 2 +- styles/src/styleTree/search.ts | 2 +- styles/src/system/types.ts | 10 +++++----- 41 files changed, 69 insertions(+), 72 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 46a3fb5a5a7f63d5855d8fb2f8ff9023b82c9dac..d37bc7d2ed0e049f3dff7d5197900e915b733f4e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -516,7 +516,7 @@ "terminal::SendText", "\u0001" ], - // Terminal.app compatability + // Terminal.app compatibility "alt-left": [ "terminal::SendText", "\u001bb" diff --git a/assets/settings/default.json b/assets/settings/default.json index 23599c8dfb1e327d9d842b76fb8c5e2914c23b3f..8a7a215e9d2c665e1dd4f0a1c8f94928447a333a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -245,7 +245,7 @@ // copy to the system clipboard. "copy_on_select": false, // Any key-value pairs added to this list will be added to the terminal's - // enviroment. Use `:` to seperate multiple values. + // environment. Use `:` to separate multiple values. "env": { // "KEY": "value1:value2" }, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index feebbff61b93043b88ed2eaa0603018eb191fd69..bdf677512c4b0326cf2bc0e513a00bbb563f0a02 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -159,10 +159,7 @@ impl Bundle { fn path(&self) -> &Path { match self { Self::App { app_bundle, .. } => app_bundle, - Self::LocalPath { - executable: excutable, - .. - } => excutable, + Self::LocalPath { executable, .. } => executable, } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index eb1755a9ff3c25494c93c5e320362d08da45c56b..720a73f477e1c5ceb4e8888de95afefe937f1589 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -472,7 +472,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - //TODO: Ensure this button has consistant width for both text variations + //TODO: Ensure this button has consistent width for both text variations let style = titlebar.share_button.style_for(state, false); Label::new(label, style.text.clone()) .contained() diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 7834603552fa14a36d81109c2a8030a8fef4efee..04e8764f7b0d5d72f50fd36f3b83c2b7ab25bea3 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -375,7 +375,7 @@ impl Copilot { server .on_notification::(|params, _cx| { match params.level { - // Copilot is pretty agressive about logging + // Copilot is pretty aggressive about logging 0 => debug!("copilot: {}", params.message), 1 => debug!("copilot: {}", params.message), _ => error!("copilot: {}", params.message), diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b0483db68dcb781619188791b3f9b3630e902584..a594af51a6307bbfda7b421c8a723e0c0e3563ef 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -475,7 +475,7 @@ impl DisplaySnapshot { }) } - /// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from` + /// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from` /// Stops if `condition` returns false for any of the character position pairs observed. pub fn find_while<'a>( &'a self, @@ -486,7 +486,7 @@ impl DisplaySnapshot { Self::find_internal(self.chars_at(from), target.chars().collect(), condition) } - /// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from` + /// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from` /// Stops if `condition` returns false for any of the character position pairs observed. pub fn reverse_find_while<'a>( &'a self, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af7344a91f230b6ace019a179ea242011958c597..d98bd92532238337381dfb100f38b44c18369d1d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2539,7 +2539,7 @@ impl Editor { .read(cx) .text_anchor_for_position(position.clone(), cx)?; - // OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances, + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, // hence we do LSP request & edit on host side only — add formats to host's history. let push_to_lsp_host_history = true; // If this is not the host, append its history with new edits. @@ -7913,13 +7913,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } pub fn highlight_diagnostic_message( - inital_highlights: Vec, + initial_highlights: Vec, message: &str, ) -> (String, Vec) { let mut message_without_backticks = String::new(); let mut prev_offset = 0; let mut inside_block = false; - let mut highlights = inital_highlights; + let mut highlights = initial_highlights; for (match_ix, (offset, _)) in message .match_indices('`') .chain([(message.len(), "")]) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 969b9c882d937a165473e06715b29f6352ed4fbc..35d61bf0cf666e009dd6a6f685fbfb5e97559b30 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4321,7 +4321,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - // Set rust language override and assert overriden tabsize is sent to language server + // Set rust language override and assert overridden tabsize is sent to language server update_test_settings(cx, |settings| { settings.languages.insert( "Rust".into(), @@ -4435,7 +4435,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { ); assert!(!cx.read(|cx| editor.is_dirty(cx))); - // Set rust language override and assert overriden tabsize is sent to language server + // Set rust language override and assert overridden tabsize is sent to language server update_test_settings(cx, |settings| { settings.languages.insert( "Rust".into(), @@ -4776,7 +4776,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { two threeˇ "}, - "overlapping aditional edit", + "overlapping additional edit", ), ( indoc! {" diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6be065084814e570a0411ffb91df4a4eb380e1ac..a74775e7f6d5042e69669d731474cc465a0abafc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3080,7 +3080,7 @@ mod tests { editor_width: f32, ) -> Vec { info!( - "Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'" + "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" ); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&input_text, cx); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index af0003ec2dde8f6fe302c9fb7c0aabfdcf91c2ff..28b60f4c025b9a3cb94ce439a6e8e87f3762f6e9 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -5332,7 +5332,7 @@ mod tests { assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occured since + // and also any individual buffer edits that have occurred since // that transaction. multibuffer.undo(cx); assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 8a3e08bea570ec745b527c24951540026caa4879..9b568e4a4fe27ed36c47c9fb02561d4a63cfb237 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -48,8 +48,8 @@ pub fn marked_display_snapshot( } pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext) { - let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true); - assert_eq!(editor.text(cx), umarked_text); + let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); + assert_eq!(editor.text(cx), unmarked_text); editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 99562405b5b909e30e27df8b713e13c7e6c30cc5..4c6b6c24d300273b1cbe3f2386cc51ba19cdc5d5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -32,7 +32,7 @@ use repository::{FakeGitRepositoryState, GitFileStatus}; use std::sync::Weak; lazy_static! { - static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); } #[derive(Clone, Copy, Debug, PartialEq)] @@ -77,13 +77,13 @@ impl LineEnding { } pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { *text = replaced; } } pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { replaced.into() } else { text diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d0388a9aedebbec4618fa02b22b335e2a659c5aa..882800f128abe8d41dd48e83a694b48d0020778b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -6335,9 +6335,9 @@ mod tests { #[crate::test(self)] async fn test_labeled_tasks(cx: &mut TestAppContext) { assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next())); - let (mut sender, mut reciever) = postage::oneshot::channel::<()>(); + let (mut sender, mut receiver) = postage::oneshot::channel::<()>(); let task = cx - .update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await })); + .update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await })); assert_eq!( Some("Test Label"), diff --git a/crates/gpui/src/keymap_matcher.rs b/crates/gpui/src/keymap_matcher.rs index 0c088f9728977379ee0f8fcd80d5d9d1209f5d20..bc70638b2c077bf5aee53f9fedd22e96938d6b4b 100644 --- a/crates/gpui/src/keymap_matcher.rs +++ b/crates/gpui/src/keymap_matcher.rs @@ -67,7 +67,7 @@ impl KeymapMatcher { /// MatchResult::Pending => /// There exist bindings which are still waiting for more keys. /// MatchResult::Complete(matches) => - /// 1 or more bindings have recieved the necessary key presses. + /// 1 or more bindings have received the necessary key presses. /// The order of the matched actions is by position of the matching first, // and order in the keymap second. pub fn push_keystroke( diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 332e789b4d7e364ab24c382a4daa9928291967cd..1a953b0bf21c0f3ed7d162a516657ce69cbb6a0b 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -264,7 +264,7 @@ impl settings::Setting for AllLanguageSettings { let mut root_schema = generator.root_schema_for::(); // Create a schema for a 'languages overrides' object, associating editor - // settings with specific langauges. + // settings with specific languages. assert!(root_schema .definitions .contains_key("LanguageSettingsContent")); diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index ce1196e946cacdaf6bef12db21c4dfcd3b4a157b..16313db49881070f6c0baac9b71b317761079830 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -773,7 +773,7 @@ impl<'a> SyntaxMapCaptures<'a> { } in layers { let grammar = match &language.grammar { - Some(grammer) => grammer, + Some(grammar) => grammar, None => continue, }; let query = match query(&grammar) { @@ -896,7 +896,7 @@ impl<'a> SyntaxMapMatches<'a> { } in layers { let grammar = match &language.grammar { - Some(grammer) => grammer, + Some(grammar) => grammar, None => continue, }; let query = match query(&grammar) { diff --git a/crates/plugin_macros/src/lib.rs b/crates/plugin_macros/src/lib.rs index 3f708658fd4f368088f58a837087ef89d722a9ba..2fe8b31b613b6fdf9206c39bac3d9083c3111a6e 100644 --- a/crates/plugin_macros/src/lib.rs +++ b/crates/plugin_macros/src/lib.rs @@ -11,7 +11,7 @@ use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Ty /// "Hello from Wasm".into() /// } /// ``` -/// This macro makes a function defined guest-side avaliable host-side. +/// This macro makes a function defined guest-side available host-side. /// Note that all arguments and return types must be `serde`. #[proc_macro_attribute] pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { @@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { /// #[import] /// pub fn operating_system_name() -> String; /// ``` -/// This macro makes a function defined host-side avaliable guest-side. +/// This macro makes a function defined host-side available guest-side. /// Note that all arguments and return types must be `serde`. /// All that's provided is a signature, as the function is implemented host-side. #[proc_macro_attribute] diff --git a/crates/plugin_runtime/OPAQUE.md b/crates/plugin_runtime/OPAQUE.md index c99224edeb45d54e29deb75d77ac970c75839345..52ee75dbf9bab78811d0ad858d2036889d9a0517 100644 --- a/crates/plugin_runtime/OPAQUE.md +++ b/crates/plugin_runtime/OPAQUE.md @@ -127,7 +127,7 @@ use plugin_handles::RopeHandle; pub fn append(rope: RopeHandle, string: &str); ``` -This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle. +This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle. To illustrate that point, here's an example. First, we'd define a plugin-side function as follows: @@ -177,7 +177,7 @@ So here's what calling `append_newline` would do, from the top: 6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource. -Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. +Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. ## Final Notes diff --git a/crates/plugin_runtime/src/plugin.rs b/crates/plugin_runtime/src/plugin.rs index bfcb024faa4514da0cc19186a38e0bc971c8d221..8070539b35da3ab2ec438c3886defe3e8cff8578 100644 --- a/crates/plugin_runtime/src/plugin.rs +++ b/crates/plugin_runtime/src/plugin.rs @@ -132,7 +132,7 @@ impl PluginBuilder { "env", &format!("__{}", name), move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once avaliable + // TODO: use try block once available let result: Result<(WasiBuffer, Memory, _), Trap> = (|| { // grab a handle to the memory let plugin_memory = match caller.get_export("memory") { @@ -211,7 +211,7 @@ impl PluginBuilder { "env", &format!("__{}", name), move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { - // TODO: use try block once avaliable + // TODO: use try block once available let result: Result<(WasiBuffer, Memory, Vec), Trap> = (|| { // grab a handle to the memory let plugin_memory = match caller.get_export("memory") { @@ -297,7 +297,7 @@ pub enum PluginBinary<'a> { Precompiled(&'a [u8]), } -/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface. +/// Represents a WebAssembly plugin, with access to the WebAssembly System Interface. /// Build a new plugin using [`PluginBuilder`]. pub struct Plugin { store: Store, @@ -559,7 +559,7 @@ impl Plugin { .ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?; // write the argument to linear memory - // this returns a (ptr, lentgh) pair + // this returns a (ptr, length) pair let arg_buffer = Self::bytes_to_buffer( self.store.data().alloc_buffer(), &mut plugin_memory, @@ -569,7 +569,7 @@ impl Plugin { .await?; // call the function, passing in the buffer and its length - // this returns a ptr to a (ptr, lentgh) pair + // this returns a ptr to a (ptr, length) pair let result_buffer = handle .function .call_async(&mut self.store, arg_buffer.into_u64()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7ce07959d7972c76f32cb43dce5bf832d5df23d7..b0d0d670bae382c0e42e8bd75b7a3b8a59a904ff 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4052,7 +4052,7 @@ impl Project { let end_within = range.start.cmp(&primary.end, buffer).is_le() && range.end.cmp(&primary.end, buffer).is_ge(); - //Skip addtional edits which overlap with the primary completion edit + //Skip additional edits which overlap with the primary completion edit //https://github.com/zed-industries/zed/pull/1871 if !start_within && !end_within { buffer.edit([(range, text)], None, cx); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a33a0fc050177db96dfbb56a901331bb6928e5ff..97656b7b10053e4d6b7a30982719ebd75adab057 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -157,7 +157,7 @@ impl RepositoryEntry { self.statuses .iter_from(&repo_path) .take_while(|(key, _)| key.starts_with(&repo_path)) - // Short circut once we've found the highest level + // Short circuit once we've found the highest level .take_until(|(_, status)| status == &&GitFileStatus::Conflict) .map(|(_, status)| status) .reduce( @@ -3623,7 +3623,7 @@ pub trait WorktreeHandle { impl WorktreeHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that - // occurred before the worktree was constructed. These events can cause the worktree to perfrom + // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. // // This function mutates the worktree's directory and waits for those mutations to be picked up, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 71271d4e7c736e839fdc2fcc00ccc5678e46f741..0dc5dad3bf8d43edf3cb4c18507a7389ca65c2e1 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -276,7 +276,7 @@ mod tests { .await .unwrap(); - // Set up fake langauge server to return fuzzy matches against + // Set up fake language server to return fuzzy matches against // a fixed set of symbol names. let fake_symbols = [ symbol("one", "/external"), diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8351cdfd2dfe6a61bcccaad4d7a8dc40ff00bfc2..ca3ec7cafb899a8ccbec6aec548c79d97ea30daa 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -476,7 +476,7 @@ message Symbol { string name = 4; int32 kind = 5; string path = 6; - // Cannot use generate anchors for unopend files, + // Cannot use generate anchors for unopened files, // so we are forced to use point coords instead PointUtf16 start = 7; PointUtf16 end = 8; diff --git a/crates/rpc/src/auth.rs b/crates/rpc/src/auth.rs index 39c8c39a4423de758807e67751a313e091e4f929..ac7bbcebecd98ebfb00a5dad4b92c16eddb3e628 100644 --- a/crates/rpc/src/auth.rs +++ b/crates/rpc/src/auth.rs @@ -42,7 +42,7 @@ impl PublicKey { } impl PrivateKey { - /// Decrypt a base64-encoded string that was encrypted by the correspoding public key. + /// Decrypt a base64-encoded string that was encrypted by the corresponding public key. pub fn decrypt_string(&self, encrypted_string: &str) -> Result { let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE) .context("failed to base64-decode encrypted string")?; diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index c3fe4657ee9182dbf1c229e32b19ebf3c30c0ec7..ffadb3af41d165737af55538661ec7100e62daeb 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -160,7 +160,7 @@ impl ThreadSafeConnection { // Create a one shot channel for the result of the queued write // so we can await on the result - let (sender, reciever) = oneshot::channel(); + let (sender, receiver) = oneshot::channel(); let thread_safe_connection = (*self).clone(); write_channel(Box::new(move || { @@ -168,7 +168,7 @@ impl ThreadSafeConnection { let result = connection.with_write(|connection| callback(connection)); sender.send(result).ok(); })); - reciever.map(|response| response.expect("Write queue unexpectedly closed")) + receiver.map(|response| response.expect("Write queue unexpectedly closed")) } pub(crate) fn create_connection( @@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor { use std::sync::mpsc::channel; Box::new(|| { - let (sender, reciever) = channel::(); + let (sender, receiver) = channel::(); thread::spawn(move || { - while let Ok(write) = reciever.recv() { + while let Ok(write) = receiver.recv() { write() } }); diff --git a/crates/terminal/src/mappings/colors.rs b/crates/terminal/src/mappings/colors.rs index 5bed4b573c4b97ada6f6c0c3a5ef649c1d1a7e92..3f776251b52693893f58eb6868a53ad995f5ab84 100644 --- a/crates/terminal/src/mappings/colors.rs +++ b/crates/terminal/src/mappings/colors.rs @@ -45,7 +45,7 @@ pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { } ///Converts an 8 bit ANSI color to it's GPUI equivalent. -///Accepts usize for compatability with the alacritty::Colors interface, +///Accepts usize for compatibility with the alacritty::Colors interface, ///Other than that use case, should only be called with values in the [0,255] range pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { match index { @@ -78,7 +78,7 @@ pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale } - //For compatability with the alacritty::Colors interface + //For compatibility with the alacritty::Colors interface 256 => style.foreground, 257 => style.background, 258 => style.cursor, diff --git a/crates/terminal_view/README.md b/crates/terminal_view/README.md index 272212a538a06766d254b6bbd675643af62e1175..019460067ebf20f991b18a9cd5ce262ae7675504 100644 --- a/crates/terminal_view/README.md +++ b/crates/terminal_view/README.md @@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal: 3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. -4. Pasted text has a seperate pathway. +4. Pasted text has a separate pathway. Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal \ No newline at end of file diff --git a/crates/terminal_view/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh index 99e3d8c9f9d3ae7d37e006fe9af328f0c7ea849c..8a53f3bc025842d900b5b4797eefc6d8a0946120 100755 --- a/crates/terminal_view/scripts/print256color.sh +++ b/crates/terminal_view/scripts/print256color.sh @@ -40,7 +40,7 @@ function contrast_colour { # Uncomment the below for more precise luminance calculations - # # Calculate percieved brightness + # # Calculate perceived brightness # # See https://www.w3.org/TR/AERT#color-contrast # # and http://www.itu.int/rec/R-REC-BT.601 # # Luminance is in range 0..5000 as each value is 0..5 diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 18c85db980ba14aab27ad91254ee022db57b1a0f..2f2ff2cdc3bde8bb5ee1c4a32978ed28518bd94f 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -34,7 +34,7 @@ use std::{mem, ops::Range}; use crate::TerminalView; -///The information generated during layout that is nescessary for painting +///The information generated during layout that is necessary for painting pub struct LayoutState { cells: Vec, rects: Vec, @@ -206,7 +206,7 @@ impl TerminalElement { //Expand background rect range { if matches!(bg, Named(NamedColor::Background)) { - //Continue to next cell, resetting variables if nescessary + //Continue to next cell, resetting variables if necessary cur_alac_color = None; if let Some(rect) = cur_rect { rects.push(rect); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9478b3eef14221782050ca9ddf269878a91fef28..c40a1a7ccd3d493e16e6a4b03a2d36e21a0b0c14 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -804,7 +804,7 @@ mod tests { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); - //Make sure enviroment is as expeted + //Make sure environment is as expected assert!(active_entry.is_none()); assert!(workspace.worktrees(cx).next().is_none()); @@ -825,7 +825,7 @@ mod tests { let workspace = workspace.read(cx); let active_entry = project.read(cx).active_entry(); - //Make sure enviroment is as expeted + //Make sure environment is as expected assert!(active_entry.is_none()); assert!(workspace.worktrees(cx).next().is_some()); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4b8266120a3682e118c497c3d4ba3cea7cd9e2df..7e26e0a29649681346ce6148a71f7dc1058467da 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -193,7 +193,7 @@ fn test_line_len() { } #[test] -fn test_common_prefix_at_positionn() { +fn test_common_prefix_at_position() { let text = "a = str; b = δα"; let buffer = Buffer::new(0, 0, text.into()); @@ -216,7 +216,7 @@ fn test_common_prefix_at_positionn() { empty_range_after(text, "str"), ); - // prefix matching is case insenstive. + // prefix matching is case insensitive. assert_eq!( buffer.common_prefix_at(offset1, "Strαngε"), range_of(text, "str"), diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 83a18a1ac9f585fe80e657752983b120b021dfd5..91be1022a3dd7bb448dbbc447dff7175dfdceeb9 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -756,7 +756,7 @@ mod test { ˇ The quick"}) .await; - // Indoc disallows trailing whitspace. + // Indoc disallows trailing whitespace. cx.assert(" ˇ \nThe quick").await; } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 538e83a8036f114c84c34f1c4e833ed449fa7c45..c3916722ddeaad974747a63729058b73c7d7d544 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -29,7 +29,7 @@ use tokio::{ use crate::state::Mode; use collections::VecDeque; -// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock +// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock // to ensure we are only constructing one neovim connection at a time. #[cfg(feature = "neovim")] lazy_static! { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1fa8c15f9bc1f5399fbcda51cb68d2b594543c46..b784cb39b8297d176fc1f0667b09054a86d127d0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1022,7 +1022,7 @@ impl Pane { let is_active_item = target_item_id == active_item_id; let target_pane = cx.weak_handle(); - // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab + // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab self.tab_context_menu.update(cx, |menu, cx| { menu.show( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 795747cfcc5045a8c554b9ca8f206b57bb313dc8..95d0bf653855bc5effbfe015e38ba7232fd9ccd3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4424,7 +4424,7 @@ mod tests { assert!(!panel.has_focus(cx)); }); - // Transfering focus back to the panel keeps it zoomed + // Transferring focus back to the panel keeps it zoomed workspace.update(cx, |workspace, cx| { workspace.toggle_panel_focus::(cx); }); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 429a5d942151cd9dbd3661b2ebb6bbc16a6505e3..a25d7d02ea550b84a88482be86d3501ecea8032d 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -207,7 +207,7 @@ impl LspAdapter for EsLintLspAdapter { http: Arc, ) -> Result> { // At the time of writing the latest vscode-eslint release was released in 2020 and requires - // special custom LSP protocol extensions be handled to fully initalize. Download the latest + // special custom LSP protocol extensions be handled to fully initialize. Download the latest // prerelease instead to sidestep this issue let release = latest_github_release("microsoft/vscode-eslint", true, http).await?; Ok(Box::new(GitHubLspBinaryVersion { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6248ccf4f0bf888028a7dae775f7e7e0bcbefdae..18ee4baeb23b3884249707b17f18e536f29bac93 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1,4 +1,4 @@ -// Allow binary to be called Zed for a nice application menu when running executable direcly +// Allow binary to be called Zed for a nice application menu when running executable directly #![allow(non_snake_case)] use anyhow::{anyhow, Context, Result}; diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts index 9a053e9c0c9ca7732a71b149a884df4a11b9391c..e77cb11bb9ec3c8bfbc381d5942c76709082deeb 100644 --- a/styles/src/buildLicenses.ts +++ b/styles/src/buildLicenses.ts @@ -23,7 +23,7 @@ function checkLicenses( licenses: string[] ) { for (const { meta } of schemeMetaWithLicense) { - // FIXME: Add support for conjuctions and conditions + // FIXME: Add support for conjunctions and conditions if (licenses.indexOf(meta.license.SPDX) < 0) { throw Error( `License for theme ${meta.name} (${meta.license.SPDX}) is not supported` diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index fa0aec2012dee96d07e4359761c1d723fc41f0bd..66074aa684bc5719ac7ce7061e4afb8b28b7e95e 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -112,7 +112,7 @@ export default function editor(colorScheme: ColorScheme) { widthEm: 0.15, cornerRadius: 0.05, }, - /** Highlights matching occurences of what is under the cursor + /** Highlights matching occurrences of what is under the cursor * as well as matched brackets */ documentHighlightReadBackground: withOpacity( diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts index 6fc8a95d7dbde571103d6d1ebebf4265837ba1b4..2094a2e369739327d968a6616ebab0b373f916f0 100644 --- a/styles/src/styleTree/search.ts +++ b/styles/src/styleTree/search.ts @@ -33,7 +33,7 @@ export default function search(colorScheme: ColorScheme) { }; return { - // TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive matchBackground: withOpacity(foreground(layer, "accent"), 0.4), optionButton: { ...text(layer, "mono", "on"), diff --git a/styles/src/system/types.ts b/styles/src/system/types.ts index 8bfa2dd7dbf9608f4549fdc2adde412c7da5022d..8de65a37eb131ba412fe269529106d58070669ee 100644 --- a/styles/src/system/types.ts +++ b/styles/src/system/types.ts @@ -1,6 +1,6 @@ import { Curve } from "./ref/curves" -export interface ColorAccessiblityValue { +export interface ColorAccessibilityValue { value: number aaPass: boolean aaaPass: boolean @@ -12,14 +12,14 @@ export interface ColorAccessiblityValue { * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette. * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information. */ -export interface ColorAccessiblity { - black: ColorAccessiblityValue - white: ColorAccessiblityValue +export interface ColorAccessibility { + black: ColorAccessibilityValue + white: ColorAccessibilityValue } export type Color = { step: number - contrast: ColorAccessiblity + contrast: ColorAccessibility hex: string lch: number[] rgba: number[] From 5790d6993e79b8200c14d2ac0a7cbd2cda54b670 Mon Sep 17 00:00:00 2001 From: Julia Date: Sun, 4 Jun 2023 23:34:22 -0400 Subject: [PATCH 056/101] Update rust-bindgen dev-dependencies --- Cargo.lock | 113 ++++++++++++++-------------------------- crates/gpui/Cargo.toml | 2 +- crates/media/Cargo.toml | 2 +- 3 files changed, 41 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07a859b058692c2ba7571f75b7cc9a9dd31374cf..7f9a5530132f36be1fe38fd5428611aaf29b2aed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,15 +180,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "anyhow" version = "1.0.71" @@ -404,7 +395,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -452,7 +443,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -495,7 +486,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -715,24 +706,24 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.59.2" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap 2.34.0", - "env_logger 0.9.3", "lazy_static", "lazycell", "log", "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", + "syn 2.0.18", "which", ] @@ -1085,21 +1076,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "3.2.25" @@ -1112,9 +1088,9 @@ dependencies = [ "clap_lex", "indexmap", "once_cell", - "strsim 0.10.0", + "strsim", "termcolor", - "textwrap 0.16.0", + "textwrap", ] [[package]] @@ -1144,7 +1120,7 @@ name = "cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 3.2.25", + "clap", "core-foundation", "core-services", "dirs 3.0.2", @@ -1254,7 +1230,7 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", - "clap 3.2.25", + "clap", "client", "collections", "ctor", @@ -1797,7 +1773,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -1814,7 +1790,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2587,7 +2563,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4348,7 +4324,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -4793,6 +4769,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +dependencies = [ + "proc-macro2", + "syn 2.0.18", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -4828,9 +4814,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -5107,9 +5093,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -6053,7 +6039,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6096,7 +6082,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -6583,12 +6569,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -6660,9 +6640,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -6848,15 +6828,6 @@ dependencies = [ "util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "textwrap" version = "0.16.0" @@ -6930,7 +6901,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7088,7 +7059,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7276,7 +7247,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -7837,12 +7808,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -8006,7 +7971,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] @@ -8040,7 +8005,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -8907,7 +8872,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index c1dc13084e804d10d0f2134d0160c165098c4f5f..31b7db008d28d56849c7ce36eb3f23336fe82fad 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] } waker-fn = "1.1.0" [build-dependencies] -bindgen = "0.59.2" +bindgen = "0.65.1" cc = "1.0.67" [dev-dependencies] diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 22a494d754ca09c4feef16fa3dc48c505dab48ff..3825897e7b5be2d871bac7cd4a778517191ed1eb 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -18,4 +18,4 @@ metal = "0.21.0" objc = "0.2" [build-dependencies] -bindgen = "0.59.2" +bindgen = "0.65.1" From bef6932da794877ab1011d1b3cacf28a9e4d17d0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Jun 2023 11:25:21 +0200 Subject: [PATCH 057/101] Avoid accidentally taking the `api_key` when requesting an assist --- crates/ai/src/ai.rs | 4 +--- crates/ai/src/assistant.rs | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index a5d5666a72fe2cd08431ccbec5cd94e6f603254d..11704de03e6c8b5ecfb2d944e68bf545b45da742 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -2,11 +2,9 @@ pub mod assistant; mod assistant_settings; pub use assistant::AssistantPanel; -use gpui::{actions, AppContext}; +use gpui::AppContext; use serde::{Deserialize, Serialize}; -actions!(ai, [Assist]); - // Data types for chat completion requests #[derive(Serialize)] struct OpenAIRequest { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 68f722f1eef128ce4d0bdb4b51f5157e5921d713..7020f9f38cc081ecc1baab6f2af318daf571fd0d 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -16,7 +16,7 @@ use gpui::{ use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; -use std::{cell::Cell, io, rc::Rc, sync::Arc, time::Duration}; +use std::{cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; use tiktoken_rs::model::get_context_size; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ @@ -62,7 +62,7 @@ pub struct AssistantPanel { width: Option, height: Option, pane: ViewHandle, - api_key: Rc>>, + api_key: Rc>>, api_key_editor: Option>, has_read_credentials: bool, languages: Arc, @@ -136,7 +136,7 @@ impl AssistantPanel { let mut this = Self { pane, - api_key: Rc::new(Cell::new(None)), + api_key: Rc::new(RefCell::new(None)), api_key_editor: None, has_read_credentials: false, languages: workspace.app_state().languages.clone(), @@ -199,7 +199,7 @@ impl AssistantPanel { cx.platform() .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes()) .log_err(); - self.api_key.set(Some(api_key)); + *self.api_key.borrow_mut() = Some(api_key); self.api_key_editor.take(); cx.focus_self(); cx.notify(); @@ -333,7 +333,7 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active { - if self.api_key.clone().take().is_none() && !self.has_read_credentials { + if self.api_key.borrow().is_none() && !self.has_read_credentials { self.has_read_credentials = true; let api_key = if let Some((_, api_key)) = cx .platform() @@ -346,7 +346,7 @@ impl Panel for AssistantPanel { None }; if let Some(api_key) = api_key { - self.api_key.set(Some(api_key)); + *self.api_key.borrow_mut() = Some(api_key); } else if self.api_key_editor.is_none() { self.api_key_editor = Some(build_api_key_editor(cx)); cx.notify(); @@ -403,7 +403,7 @@ struct Assistant { token_count: Option, max_token_count: usize, pending_token_count: Task>, - api_key: Rc>>, + api_key: Rc>>, _subscriptions: Vec, } @@ -413,7 +413,7 @@ impl Entity for Assistant { impl Assistant { fn new( - api_key: Rc>>, + api_key: Rc>>, language_registry: Arc, cx: &mut ModelContext, ) -> Self { @@ -504,7 +504,8 @@ impl Assistant { stream: true, }; - if let Some(api_key) = self.api_key.clone().take() { + let api_key = self.api_key.borrow().clone(); + if let Some(api_key) = api_key { let stream = stream_completion(api_key, cx.background().clone(), request); let response = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); @@ -600,7 +601,7 @@ struct AssistantEditor { impl AssistantEditor { fn new( - api_key: Rc>>, + api_key: Rc>>, language_registry: Arc, cx: &mut ViewContext, ) -> Self { @@ -846,7 +847,9 @@ async fn stream_completion( while let Some(line) = lines.next().await { if let Some(event) = parse_line(line).transpose() { - tx.unbounded_send(event).log_err(); + if tx.unbounded_send(event).is_err() { + break; + } } } From c872f581d14ab26b113ae25a0f930edd8ca0c700 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Jun 2023 12:00:40 +0200 Subject: [PATCH 058/101] Query `rect_for_text_range` on focused view instead of root element This was causing IME input to be drawn in the wrong place when there were splits or panels in the window. --- crates/gpui/src/app/window.rs | 4 ++-- crates/gpui/src/app/window_input_handler.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 30d58cdd726e532599a743caa5aae3c01917340f..cfcef626dfa2a5baf36a230eebcd1e5ae2d23495 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> { } pub fn rect_for_text_range(&self, range_utf16: Range) -> Option { - let root_view_id = self.window.root_view().id(); + let focused_view_id = self.window.focused_view_id?; self.window .rendered_views - .get(&root_view_id)? + .get(&focused_view_id)? .rect_for_text_range(range_utf16, self) .log_err() .flatten() diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs index 938dbe1a0390c499575975bfdcda1bf8f2139609..8ee9f7eeff5c2a3e4f4d63ca55c9338f02e3ed69 100644 --- a/crates/gpui/src/app/window_input_handler.rs +++ b/crates/gpui/src/app/window_input_handler.rs @@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler { fn rect_for_range(&self, range_utf16: Range) -> Option { self.app - .borrow_mut() - .update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) + .borrow() + .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) .flatten() } } From 917d8949b78c2c1a704b0882dd79fa79310ce6ef Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Mon, 5 Jun 2023 14:17:48 -0400 Subject: [PATCH 059/101] Move app version into panic object --- crates/zed/src/main.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 18ee4baeb23b3884249707b17f18e536f29bac93..a3c359c189500343daabf280b3a0c2b42fb847ed 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -372,6 +372,7 @@ struct Panic { #[serde(skip_serializing_if = "Option::is_none")] location_data: Option, backtrace: Vec, + app_version: String, release_channel: String, os_name: String, os_version: Option, @@ -383,8 +384,6 @@ struct Panic { #[derive(Serialize)] struct PanicRequest { panic: Panic, - // TODO: Move to Panic struct, as app_version - requires changing zed.dev - version: String, token: String, } @@ -471,6 +470,7 @@ fn init_panic_hook(app: &App) { file: location.file().into(), line: location.line(), }), + app_version: app_version.clone(), release_channel: RELEASE_CHANNEL.dev_name().into(), os_name: platform.os_name().into(), os_version: platform @@ -496,8 +496,7 @@ fn init_panic_hook(app: &App) { } let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); - let panic_file_path = - paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp)); + let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); let panic_file = std::fs::OpenOptions::new() .append(true) .create(true) @@ -532,15 +531,9 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { continue; }; - let mut components = filename.split('-'); - if components.next() != Some("zed") { + if !filename.starts_with("zed") { continue; } - let version = if let Some(version) = components.next() { - version - } else { - continue; - }; if telemetry_settings.diagnostics { let panic_data_text = smol::fs::read_to_string(&child_path) @@ -549,7 +542,6 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { let body = serde_json::to_string(&PanicRequest { panic: serde_json::from_str(&panic_data_text)?, - version: version.to_string(), token: ZED_SECRET_CLIENT_TOKEN.into(), }) .unwrap(); From 70c5489c1345b4f53a0fe603356e9aeffb10337a Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Mon, 5 Jun 2023 15:09:47 -0400 Subject: [PATCH 060/101] Stop sending editor events to mixpanel --- crates/editor/src/editor.rs | 15 +++------------ crates/editor/src/editor_tests.rs | 3 ++- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d98bd92532238337381dfb100f38b44c18369d1d..f956d5725cdb7585f71948cc695a45b242a26713 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -46,9 +46,9 @@ use gpui::{ impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, - serde_json::{self, json}, - AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, - ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, + Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -7211,15 +7211,6 @@ impl Editor { .show_copilot_suggestions; let telemetry = project.read(cx).client().telemetry().clone(); - telemetry.report_mixpanel_event( - match name { - "open" => "open editor", - "save" => "save editor", - _ => name, - }, - json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }), - telemetry_settings, - ); let event = ClickhouseEvent::Editor { file_extension, vim_mode, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 35d61bf0cf666e009dd6a6f685fbfb5e97559b30..a9d100575f40871fe8dc4be3b9825b2db7e0d8de 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9,7 +9,8 @@ use gpui::{ executor::Deterministic, geometry::{rect::RectF, vector::vec2f}, platform::{WindowBounds, WindowOptions}, - serde_json, TestAppContext, + serde_json::{self, json}, + TestAppContext, }; use indoc::indoc; use language::{ From 311074e3977f9918ec2b567af6f84702024817a1 Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Mon, 5 Jun 2023 15:33:17 -0400 Subject: [PATCH 061/101] Remove code sending zed events to mixpanel --- .github/workflows/ci.yml | 1 - .github/workflows/release_actions.yml | 16 -- crates/client/src/client.rs | 18 -- crates/client/src/telemetry.rs | 202 +---------------------- crates/zed/build.rs | 3 - crates/zed/src/main.rs | 5 - script/mixpanel_release/main.py | 30 ---- script/mixpanel_release/requirements.txt | 1 - 8 files changed, 4 insertions(+), 272 deletions(-) delete mode 100644 script/mixpanel_release/main.py delete mode 100644 script/mixpanel_release/requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cd717d5c1f576668fead517290f8be36018e786..f1c16b2d4d2a540da2ff6bbd00f01323b820f12e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,6 @@ jobs: MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }} steps: - name: Install Rust run: | diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 5feb29e46930ab249a62050fcb9c7c1623ca7398..4ccab09cbe10c56f49d43e08d6483b28846021cb 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -21,19 +21,3 @@ jobs: ${{ github.event.release.body }} ``` - mixpanel_release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: "3.10.5" - architecture: "x64" - cache: "pip" - - run: pip install -r script/mixpanel_release/requirements.txt - - run: > - python script/mixpanel_release/main.py - ${{ github.event.release.tag_name }} - ${{ secrets.MIXPANEL_PROJECT_ID }} - ${{ secrets.MIXPANEL_SERVICE_ACCOUNT_USERNAME }} - ${{ secrets.MIXPANEL_SERVICE_ACCOUNT_SECRET }} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c9b83d805a4a87a65eda95f5b765c35841627d30..78bcc55e93ffe57dcec4ce760978ab2d22a321e8 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -776,15 +776,6 @@ impl Client { if credentials.is_none() && try_keychain { credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); - if read_from_keychain { - cx.read(|cx| { - self.telemetry().report_mixpanel_event( - "read credentials from keychain", - Default::default(), - *settings::get::(cx), - ); - }); - } } if credentials.is_none() { let mut status_rx = self.status(); @@ -1072,11 +1063,8 @@ impl Client { ) -> Task> { let platform = cx.platform(); let executor = cx.background(); - let telemetry = self.telemetry.clone(); let http = self.http.clone(); - let telemetry_settings = cx.read(|cx| *settings::get::(cx)); - executor.clone().spawn(async move { // Generate a pair of asymmetric encryption keys. The public key will be used by the // zed server to encrypt the user's access token, so that it can'be intercepted by @@ -1159,12 +1147,6 @@ impl Client { .context("failed to decrypt access token")?; platform.activate(true); - telemetry.report_mixpanel_event( - "authenticate with browser", - Default::default(), - telemetry_settings, - ); - Ok(Credentials { user_id: user_id.parse()?, access_token, diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index cf47700dfe26936d44676c208fee941e9e4d9565..c1d58222461e58b8ce48d7853818bc68acb35b46 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,14 +1,9 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use db::kvp::KEY_VALUE_STORE; -use gpui::{ - executor::Background, - serde_json::{self, value::Map, Value}, - AppContext, Task, -}; +use gpui::{executor::Background, serde_json, AppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; -use serde_json::json; use std::{ env, io::Write, @@ -19,7 +14,7 @@ use std::{ }; use tempfile::NamedTempFile; use util::http::HttpClient; -use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; +use util::{channel::ReleaseChannel, TryFutureExt}; use uuid::Uuid; pub struct Telemetry { @@ -37,23 +32,15 @@ struct TelemetryState { os_name: &'static str, os_version: Option>, architecture: &'static str, - mixpanel_events_queue: Vec, clickhouse_events_queue: Vec, - next_mixpanel_event_id: usize, - flush_mixpanel_events_task: Option>, flush_clickhouse_events_task: Option>, log_file: Option, is_staff: Option, } -const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track"; -const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set"; const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; lazy_static! { - static ref MIXPANEL_TOKEN: Option = std::env::var("ZED_MIXPANEL_TOKEN") - .ok() - .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string())); static ref CLICKHOUSE_EVENTS_URL: String = format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); } @@ -95,47 +82,6 @@ pub enum ClickhouseEvent { }, } -#[derive(Serialize, Debug)] -struct MixpanelEvent { - event: String, - properties: MixpanelEventProperties, -} - -#[derive(Serialize, Debug)] -struct MixpanelEventProperties { - // Mixpanel required fields - #[serde(skip_serializing_if = "str::is_empty")] - token: &'static str, - time: u128, - #[serde(rename = "distinct_id")] - installation_id: Option>, - #[serde(rename = "$insert_id")] - insert_id: usize, - // Custom fields - #[serde(skip_serializing_if = "Option::is_none", flatten)] - event_properties: Option>, - #[serde(rename = "OS Name")] - os_name: &'static str, - #[serde(rename = "OS Version")] - os_version: Option>, - #[serde(rename = "Release Channel")] - release_channel: Option<&'static str>, - #[serde(rename = "App Version")] - app_version: Option>, - #[serde(rename = "Signed In")] - signed_in: bool, -} - -#[derive(Serialize)] -struct MixpanelEngageRequest { - #[serde(rename = "$token")] - token: &'static str, - #[serde(rename = "$distinct_id")] - installation_id: Arc, - #[serde(rename = "$set")] - set: Value, -} - #[cfg(debug_assertions)] const MAX_QUEUE_LEN: usize = 1; @@ -168,29 +114,13 @@ impl Telemetry { release_channel, installation_id: None, metrics_id: None, - mixpanel_events_queue: Default::default(), clickhouse_events_queue: Default::default(), - flush_mixpanel_events_task: Default::default(), flush_clickhouse_events_task: Default::default(), - next_mixpanel_event_id: 0, log_file: None, is_staff: None, }), }); - if MIXPANEL_TOKEN.is_some() { - this.executor - .spawn({ - let this = this.clone(); - async move { - if let Some(tempfile) = NamedTempFile::new().log_err() { - this.state.lock().log_file = Some(tempfile); - } - } - }) - .detach(); - } - this } @@ -218,20 +148,9 @@ impl Telemetry { let mut state = this.state.lock(); state.installation_id = Some(installation_id.clone()); - for event in &mut state.mixpanel_events_queue { - event - .properties - .installation_id - .get_or_insert_with(|| installation_id.clone()); - } - - let has_mixpanel_events = !state.mixpanel_events_queue.is_empty(); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); - drop(state); - if has_mixpanel_events { - this.flush_mixpanel_events(); - } + drop(state); if has_clickhouse_events { this.flush_clickhouse_events(); @@ -256,37 +175,11 @@ impl Telemetry { return; } - let this = self.clone(); let mut state = self.state.lock(); - let installation_id = state.installation_id.clone(); let metrics_id: Option> = metrics_id.map(|id| id.into()); state.metrics_id = metrics_id.clone(); state.is_staff = Some(is_staff); drop(state); - - if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) { - self.executor - .spawn( - async move { - let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest { - token, - installation_id, - set: json!({ - "Staff": is_staff, - "ID": metrics_id, - "App": true - }), - }])?; - - this.http_client - .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into()) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } } pub fn report_clickhouse_event( @@ -310,7 +203,7 @@ impl Telemetry { }); if state.installation_id.is_some() { - if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { + if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { @@ -324,55 +217,6 @@ impl Telemetry { } } - pub fn report_mixpanel_event( - self: &Arc, - kind: &str, - properties: Value, - telemetry_settings: TelemetrySettings, - ) { - if !telemetry_settings.metrics { - return; - } - - let mut state = self.state.lock(); - let event = MixpanelEvent { - event: kind.into(), - properties: MixpanelEventProperties { - token: "", - time: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - installation_id: state.installation_id.clone(), - insert_id: post_inc(&mut state.next_mixpanel_event_id), - event_properties: if let Value::Object(properties) = properties { - Some(properties) - } else { - None - }, - os_name: state.os_name, - os_version: state.os_version.clone(), - release_channel: state.release_channel, - app_version: state.app_version.clone(), - signed_in: state.metrics_id.is_some(), - }, - }; - state.mixpanel_events_queue.push(event); - if state.installation_id.is_some() { - if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { - drop(state); - self.flush_mixpanel_events(); - } else { - let this = self.clone(); - let executor = self.executor.clone(); - state.flush_mixpanel_events_task = Some(self.executor.spawn(async move { - executor.timer(DEBOUNCE_INTERVAL).await; - this.flush_mixpanel_events(); - })); - } - } - } - pub fn metrics_id(self: &Arc) -> Option> { self.state.lock().metrics_id.clone() } @@ -385,44 +229,6 @@ impl Telemetry { self.state.lock().is_staff } - fn flush_mixpanel_events(self: &Arc) { - let mut state = self.state.lock(); - let mut events = mem::take(&mut state.mixpanel_events_queue); - state.flush_mixpanel_events_task.take(); - drop(state); - - if let Some(token) = MIXPANEL_TOKEN.as_ref() { - let this = self.clone(); - self.executor - .spawn( - async move { - let mut json_bytes = Vec::new(); - - if let Some(file) = &mut this.state.lock().log_file { - let file = file.as_file_mut(); - for event in &mut events { - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, event)?; - file.write_all(&json_bytes)?; - file.write(b"\n")?; - - event.properties.token = token; - } - } - - json_bytes.clear(); - serde_json::to_writer(&mut json_bytes, &events)?; - this.http_client - .post_json(MIXPANEL_EVENTS_URL, json_bytes.into()) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } - } - fn flush_clickhouse_events(self: &Arc) { let mut state = self.state.lock(); let mut events = mem::take(&mut state.clickhouse_events_queue); diff --git a/crates/zed/build.rs b/crates/zed/build.rs index bb971041a5b08f863817cef5ffeec7bee8c015fd..14bf9999fb725c6dae32bfc46341560b662f3d8b 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -1,9 +1,6 @@ fn main() { println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); - if let Ok(value) = std::env::var("ZED_MIXPANEL_TOKEN") { - println!("cargo:rustc-env=ZED_MIXPANEL_TOKEN={value}"); - } if let Ok(value) = std::env::var("ZED_PREVIEW_CHANNEL") { println!("cargo:rustc-env=ZED_PREVIEW_CHANNEL={value}"); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a3c359c189500343daabf280b3a0c2b42fb847ed..59072cc9f4b60df1f71b22c89962615edb0b085a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -171,11 +171,6 @@ fn main() { .detach(); client.telemetry().start(); - client.telemetry().report_mixpanel_event( - "start app", - Default::default(), - *settings::get::(cx), - ); let app_state = Arc::new(AppState { languages, diff --git a/script/mixpanel_release/main.py b/script/mixpanel_release/main.py deleted file mode 100644 index e2a0eeb990ce8c085fa6d41efeda2a1221c5d512..0000000000000000000000000000000000000000 --- a/script/mixpanel_release/main.py +++ /dev/null @@ -1,30 +0,0 @@ -import datetime -import sys -import requests - -def main(): - version = sys.argv[1] - version = version.removeprefix("v") - project_id = sys.argv[2] - account_username = sys.argv[3] - account_secret = sys.argv[4] - - current_datetime = datetime.datetime.now(datetime.timezone.utc) - current_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S") - - url = f"https://mixpanel.com/api/app/projects/{project_id}/annotations" - - payload = { - "date": current_datetime, - "description": version - } - - response = requests.post( - url, - auth=(account_username, account_secret), - json=payload - ) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/script/mixpanel_release/requirements.txt b/script/mixpanel_release/requirements.txt deleted file mode 100644 index 5e77405687287ea27fa8f90ccd80284639112588..0000000000000000000000000000000000000000 --- a/script/mixpanel_release/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.28.1 \ No newline at end of file From 624467ebca0bc9e7aae684b9ecfb39fed7d1539a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 5 Jun 2023 12:53:37 -0700 Subject: [PATCH 062/101] Add file and line number information to logs --- crates/util/src/util.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 9d787e1389a9742c44e505a96ddf44ee71d04671..5f738132ae0104553ae24f15736fdaaa5dd3b696 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -9,6 +9,7 @@ pub mod test; use std::{ cmp::{self, Ordering}, ops::{AddAssign, Range, RangeInclusive}, + panic::Location, pin::Pin, task::{Context, Poll}, }; @@ -129,11 +130,13 @@ where { type Ok = T; + #[track_caller] fn log_err(self) -> Option { match self { Ok(value) => Some(value), Err(error) => { - log::error!("{:?}", error); + let caller = Location::caller(); + log::error!("{}:{}: {:?}", caller.file(), caller.line(), error); None } } From 12dd91c89ce317774de0ddb58c82b28c4cae5b13 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 Jun 2023 14:12:19 -0700 Subject: [PATCH 063/101] Use local ids, not remote ids, to identify buffers to copilot --- crates/copilot/src/copilot.rs | 47 +++++++++++++++-------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 04e8764f7b0d5d72f50fd36f3b83c2b7ab25bea3..e73424f0cd36945f1a24d3155521075c7967fa80 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -4,7 +4,7 @@ mod sign_in; use anyhow::{anyhow, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use collections::HashMap; +use collections::{HashMap, HashSet}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, @@ -127,7 +127,7 @@ impl CopilotServer { struct RunningCopilotServer { lsp: Arc, sign_in_status: SignInStatus, - registered_buffers: HashMap, + registered_buffers: HashMap, } #[derive(Clone, Debug)] @@ -163,7 +163,6 @@ impl Status { } struct RegisteredBuffer { - id: u64, uri: lsp::Url, language_id: String, snapshot: BufferSnapshot, @@ -178,13 +177,13 @@ impl RegisteredBuffer { buffer: &ModelHandle, cx: &mut ModelContext, ) -> oneshot::Receiver<(i32, BufferSnapshot)> { - let id = self.id; let (done_tx, done_rx) = oneshot::channel(); if buffer.read(cx).version() == self.snapshot.version { let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); } else { let buffer = buffer.downgrade(); + let id = buffer.id(); let prev_pending_change = mem::replace(&mut self.pending_buffer_change, Task::ready(None)); self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move { @@ -268,7 +267,7 @@ pub struct Copilot { http: Arc, node_runtime: Arc, server: CopilotServer, - buffers: HashMap>, + buffers: HashSet>, } impl Entity for Copilot { @@ -559,8 +558,8 @@ impl Copilot { } pub fn register_buffer(&mut self, buffer: &ModelHandle, cx: &mut ModelContext) { - let buffer_id = buffer.read(cx).remote_id(); - self.buffers.insert(buffer_id, buffer.downgrade()); + let weak_buffer = buffer.downgrade(); + self.buffers.insert(weak_buffer.clone()); if let CopilotServer::Running(RunningCopilotServer { lsp: server, @@ -573,8 +572,7 @@ impl Copilot { return; } - let buffer_id = buffer.read(cx).remote_id(); - registered_buffers.entry(buffer_id).or_insert_with(|| { + registered_buffers.entry(buffer.id()).or_insert_with(|| { let uri: lsp::Url = uri_for_buffer(buffer, cx); let language_id = id_for_language(buffer.read(cx).language()); let snapshot = buffer.read(cx).snapshot(); @@ -592,7 +590,6 @@ impl Copilot { .log_err(); RegisteredBuffer { - id: buffer_id, uri, language_id, snapshot, @@ -603,8 +600,8 @@ impl Copilot { this.handle_buffer_event(buffer, event, cx).log_err(); }), cx.observe_release(buffer, move |this, _buffer, _cx| { - this.buffers.remove(&buffer_id); - this.unregister_buffer(buffer_id); + this.buffers.remove(&weak_buffer); + this.unregister_buffer(&weak_buffer); }), ], } @@ -619,8 +616,7 @@ impl Copilot { cx: &mut ModelContext, ) -> Result<()> { if let Ok(server) = self.server.as_running() { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) { + if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) { match event { language::Event::Edited => { let _ = registered_buffer.report_changes(&buffer, cx); @@ -674,9 +670,9 @@ impl Copilot { Ok(()) } - fn unregister_buffer(&mut self, buffer_id: u64) { + fn unregister_buffer(&mut self, buffer: &WeakModelHandle) { if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer_id) { + if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) { server .lsp .notify::( @@ -779,8 +775,7 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; let lsp = server.lsp.clone(); - let buffer_id = buffer.read(cx).remote_id(); - let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap(); + let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap(); let snapshot = registered_buffer.report_changes(buffer, cx); let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); @@ -850,7 +845,7 @@ impl Copilot { lsp_status: request::SignInStatus, cx: &mut ModelContext, ) { - self.buffers.retain(|_, buffer| buffer.is_upgradable(cx)); + self.buffers.retain(|buffer| buffer.is_upgradable(cx)); if let Ok(server) = self.server.as_running() { match lsp_status { @@ -858,7 +853,7 @@ impl Copilot { | request::SignInStatus::MaybeOk { .. } | request::SignInStatus::AlreadySignedIn { .. } => { server.sign_in_status = SignInStatus::Authorized; - for buffer in self.buffers.values().cloned().collect::>() { + for buffer in self.buffers.iter().cloned().collect::>() { if let Some(buffer) = buffer.upgrade(cx) { self.register_buffer(&buffer, cx); } @@ -866,14 +861,14 @@ impl Copilot { } request::SignInStatus::NotAuthorized { .. } => { server.sign_in_status = SignInStatus::Unauthorized; - for buffer_id in self.buffers.keys().copied().collect::>() { - self.unregister_buffer(buffer_id); + for buffer in self.buffers.iter().copied().collect::>() { + self.unregister_buffer(&buffer); } } request::SignInStatus::NotSignedIn => { server.sign_in_status = SignInStatus::SignedOut; - for buffer_id in self.buffers.keys().copied().collect::>() { - self.unregister_buffer(buffer_id); + for buffer in self.buffers.iter().copied().collect::>() { + self.unregister_buffer(&buffer); } } } @@ -896,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle, cx: &AppContext) -> lsp::Url { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { lsp::Url::from_file_path(file.abs_path(cx)).unwrap() } else { - format!("buffer://{}", buffer.read(cx).remote_id()) - .parse() - .unwrap() + format!("buffer://{}", buffer.id()).parse().unwrap() } } From 3d1ba1b36365990bd2743a347b15f81ced8387d5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 Jun 2023 14:36:21 -0700 Subject: [PATCH 064/101] Apply bounds to all windows when using start-local-collaboration script --- crates/workspace/src/workspace.rs | 53 +++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 95d0bf653855bc5effbfe015e38ba7232fd9ccd3..a429a30a396cf1971bc9b9817db5e0367a0d4934 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -765,25 +765,21 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; - let window_bounds_override = - ZED_WINDOW_POSITION - .zip(*ZED_WINDOW_SIZE) - .map(|(position, size)| { - WindowBounds::Fixed(RectF::new( - cx.platform().screens()[0].bounds().origin() + position, - size, - )) - }); - - let build_workspace = |cx: &mut ViewContext| { - Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) - }; - let workspace = requesting_window_id .and_then(|window_id| { - cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx))) + cx.update(|cx| { + cx.replace_root_view(window_id, |cx| { + Workspace::new( + workspace_id, + project_handle.clone(), + app_state.clone(), + cx, + ) + }) + }) }) .unwrap_or_else(|| { + let window_bounds_override = window_bounds_env_override(&cx); let (bounds, display) = if let Some(bounds) = window_bounds_override { (Some(bounds), None) } else { @@ -819,7 +815,14 @@ impl Workspace { // Use the serialized workspace to construct the new window cx.add_window( (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| build_workspace(cx), + |cx| { + Workspace::new( + workspace_id, + project_handle.clone(), + app_state.clone(), + cx, + ) + }, ) .1 }); @@ -3110,6 +3113,17 @@ impl Workspace { } } +fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { + ZED_WINDOW_POSITION + .zip(*ZED_WINDOW_SIZE) + .map(|(position, size)| { + WindowBounds::Fixed(RectF::new( + cx.platform().screens()[0].bounds().origin() + position, + size, + )) + }) +} + async fn open_items( serialized_workspace: Option, workspace: &WeakViewHandle, @@ -3642,8 +3656,13 @@ pub fn join_remote_project( }) .await?; + let window_bounds_override = window_bounds_env_override(&cx); let (_, workspace) = cx.add_window( - (app_state.build_window_options)(None, None, cx.platform().as_ref()), + (app_state.build_window_options)( + window_bounds_override, + None, + cx.platform().as_ref(), + ), |cx| Workspace::new(0, project, app_state.clone(), cx), ); (app_state.initialize_workspace)( From 0949ee84d8aa72872455f1c7f38c468b1e83646b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 Jun 2023 15:02:29 -0700 Subject: [PATCH 065/101] :art: Move OpenSettings action back to the zed crate --- crates/workspace/src/workspace.rs | 16 +--------------- crates/zed/src/main.rs | 6 ++---- crates/zed/src/menus.rs | 2 +- crates/zed/src/zed.rs | 11 +++++++++++ 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a429a30a396cf1971bc9b9817db5e0367a0d4934..278b02d74ebfc622043ff61b4299ec0c273ca684 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,6 @@ mod toolbar; mod workspace_settings; use anyhow::{anyhow, Context, Result}; -use assets::Assets; use call::ActiveCall; use client::{ proto::{self, PeerId}, @@ -83,7 +82,7 @@ use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::{Theme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use util::{async_iife, paths, ResultExt}; +use util::{async_iife, ResultExt}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { @@ -133,8 +132,6 @@ actions!( ] ); -actions!(zed, [OpenSettings]); - #[derive(Clone, PartialEq)] pub struct OpenPaths { pub paths: Vec, @@ -295,17 +292,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { .detach(); }); - cx.add_action( - move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content(&Assets) - .as_ref() - .into() - }) - .detach_and_log_err(cx); - }, - ); - let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 59072cc9f4b60df1f71b22c89962615edb0b085a..f5031beb2ee5774aa1299dadf7fc2756520534a4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -56,9 +56,7 @@ use fs::RealFs; #[cfg(debug_assertions)] use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; -use workspace::{ - item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace, -}; +use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use zed::{ self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, }; @@ -877,6 +875,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { ("Go to file", &file_finder::Toggle), ("Open command palette", &command_palette::Toggle), ("Open recent projects", &recent_projects::OpenRecent), - ("Change your settings", &OpenSettings), + ("Change your settings", &zed::OpenSettings), ] } diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 76e88325f5cc858091cd3fcc367340337d554a9c..83af92e26498e12f5e8789867eae23c5a3bd83b7 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -12,7 +12,7 @@ pub fn menus() -> Vec> { MenuItem::submenu(Menu { name: "Preferences", items: vec![ - MenuItem::action("Open Settings", workspace::OpenSettings), + MenuItem::action("Open Settings", super::OpenSettings), MenuItem::action("Open Key Bindings", super::OpenKeymap), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6dbddae2ada970ee754c20c2aa735c3f46c86d91..7472500d3965fb1498935a2eb55dda57589f821f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -65,6 +65,7 @@ actions!( OpenLicenses, OpenTelemetryLog, OpenKeymap, + OpenSettings, OpenDefaultSettings, OpenDefaultKeymap, IncreaseBufferFontSize, @@ -157,6 +158,16 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); }, ); + cx.add_action( + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + create_and_open_local_file(&paths::SETTINGS, cx, || { + settings::initial_user_settings_content(&Assets) + .as_ref() + .into() + }) + .detach_and_log_err(cx); + }, + ); cx.add_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { open_bundled_file( From cb975f125255713bc07b60c1d21d531f1c4e0011 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 Jun 2023 17:45:42 -0700 Subject: [PATCH 066/101] Add Zed > Preferences > Local Settings to application menu --- crates/zed/src/menus.rs | 1 + crates/zed/src/zed.rs | 55 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 83af92e26498e12f5e8789867eae23c5a3bd83b7..9112cd207b5da73f6a7d20a0436db152f98114d2 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -16,6 +16,7 @@ pub fn menus() -> Vec> { MenuItem::action("Open Key Bindings", super::OpenKeymap), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), + MenuItem::action("Open Local Settings", super::OpenLocalSettings), MenuItem::action("Select Theme", theme_selector::Toggle), ], }), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7472500d3965fb1498935a2eb55dda57589f821f..ae9370384ae4b8f5ee669f52cbeab58aa699591e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -33,7 +33,11 @@ use serde_json::to_string_pretty; use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; -use util::{channel::ReleaseChannel, paths, ResultExt}; +use util::{ + channel::ReleaseChannel, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + ResultExt, +}; use uuid::Uuid; use welcome::BaseKeymap; pub use workspace; @@ -66,6 +70,7 @@ actions!( OpenTelemetryLog, OpenKeymap, OpenSettings, + OpenLocalSettings, OpenDefaultSettings, OpenDefaultKeymap, IncreaseBufferFontSize, @@ -168,6 +173,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { .detach_and_log_err(cx); }, ); + cx.add_action(open_local_settings_file); cx.add_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { open_bundled_file( @@ -555,6 +561,53 @@ pub fn handle_keymap_file_changes( .detach(); } +fn open_local_settings_file( + workspace: &mut Workspace, + _: &OpenLocalSettings, + cx: &mut ViewContext, +) { + let project = workspace.project().clone(); + let worktree = project + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); + if let Some(worktree) = worktree { + let tree_id = worktree.read(cx).id(); + cx.spawn(|workspace, mut cx| async move { + let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; + + if let Some(dir_path) = file_path.parent() { + if worktree.read_with(&cx, |tree, _| tree.entry_for_path(dir_path).is_none()) { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + }) + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + } + + if worktree.read_with(&cx, |tree, _| tree.entry_for_path(file_path).is_none()) { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, file_path), false, cx) + }) + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + + workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path((tree_id, file_path), None, true, cx) + })? + .await?; + + anyhow::Ok(()) + }) + .detach(); + } +} + fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { workspace.with_local_workspace(cx, move |workspace, cx| { let app_state = workspace.app_state().clone(); From 296a0bf51060eab6c8eaf1b530f529ee62bce04a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 5 Jun 2023 18:09:42 -0700 Subject: [PATCH 067/101] Populate created local settings file with an empty JSON object and comments --- assets/settings/initial_local_settings.json | 11 +++++++++ crates/collab/src/rpc.rs | 4 +-- crates/settings/src/settings.rs | 19 ++++++++++++--- crates/settings/src/settings_file.rs | 14 +++-------- crates/zed/src/zed.rs | 27 ++++++++++++++++++--- 5 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 assets/settings/initial_local_settings.json diff --git a/assets/settings/initial_local_settings.json b/assets/settings/initial_local_settings.json new file mode 100644 index 0000000000000000000000000000000000000000..69be683aa81d758e114f02b9bf41d45dcfe32d81 --- /dev/null +++ b/assets/settings/initial_local_settings.json @@ -0,0 +1,11 @@ +// Folder-specific Zed settings +// +// A subset of Zed's settings can be configured on a per-folder basis. +// +// For information on how to configure Zed, see the Zed +// documentation: https://zed.dev/docs/configuring-zed +// +// To see all of Zed's default settings without changing your +// custom settings, run the `open default settings` command +// from the command palette or from `Zed` application menu. +{} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 61c6123a827e897d9bf8c5db15e9e79fdbd4b951..8d210513c2d3dc32fdac676b4953147cc4f0208e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1424,7 +1424,7 @@ async fn join_project( )?; } - for settings_file in dbg!(worktree.settings_files) { + for settings_file in worktree.settings_files { session.peer.send( session.connection_id, proto::UpdateWorktreeSettings { @@ -1554,8 +1554,6 @@ async fn update_worktree_settings( message: proto::UpdateWorktreeSettings, session: Session, ) -> Result<()> { - dbg!(&message); - let guest_connection_ids = session .db() .await diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 840797c6ad644cc851c298712e7db080a0650d78..1c131e5916c5d11746881d729cf93a254d0effd0 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -9,10 +9,23 @@ pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; use std::{borrow::Cow, str}; pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json"; -pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; +const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; +const INITIAL_LOCAL_SETTINGS_ASSET_PATH: &str = "settings/initial_local_settings.json"; -pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { - match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { +pub fn default_settings() -> Cow<'static, str> { + asset_str(&assets::Assets, DEFAULT_SETTINGS_ASSET_PATH) +} + +pub fn initial_user_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> { + asset_str(assets, INITIAL_USER_SETTINGS_ASSET_PATH) +} + +pub fn initial_local_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> { + asset_str(assets, INITIAL_LOCAL_SETTINGS_ASSET_PATH) +} + +fn asset_str<'a>(assets: &'a dyn AssetSource, path: &str) -> Cow<'a, str> { + match assets.load(path).unwrap() { Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 3505330eda6fb94cc4ded9d90f313c40a11e7866..edd4fe0d9dea93cb7444656772311f6dbd439e9b 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,11 +1,10 @@ -use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH}; +use crate::{settings_store::SettingsStore, Setting}; use anyhow::Result; use assets::Assets; use fs::Fs; use futures::{channel::mpsc, StreamExt}; -use gpui::{executor::Background, AppContext, AssetSource}; +use gpui::{executor::Background, AppContext}; use std::{ - borrow::Cow, io::ErrorKind, path::{Path, PathBuf}, str, @@ -28,19 +27,12 @@ pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppCo cx.global::().get(location) } -pub fn default_settings() -> Cow<'static, str> { - match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } -} - pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; #[cfg(any(test, feature = "test-support"))] pub fn test_settings() -> String { let mut value = crate::settings_store::parse_json_with_comments::( - default_settings().as_ref(), + crate::default_settings().as_ref(), ) .unwrap(); util::merge_non_null_json_value_into( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index ae9370384ae4b8f5ee669f52cbeab58aa699591e..f0e88133f3e20c668b2c9c86108ed3abfe1f712e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -30,7 +30,9 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH}; +use settings::{ + initial_local_settings_content, KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH, +}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ @@ -596,11 +598,30 @@ fn open_local_settings_file( .await?; } - workspace + let editor = workspace .update(&mut cx, |workspace, cx| { workspace.open_path((tree_id, file_path), None, true, cx) })? - .await?; + .await? + .downcast::() + .ok_or_else(|| anyhow!("unexpected item type"))?; + + editor + .downgrade() + .update(&mut cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit( + [(0..0, initial_local_settings_content(&Assets))], + None, + cx, + ) + }); + } + } + }) + .ok(); anyhow::Ok(()) }) From 23836eb25119538283a38d697d7606a1fbcf7409 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 5 Jun 2023 22:58:08 -0600 Subject: [PATCH 068/101] Not working yet: Remove empty messages unless they contain the cursor Problem is, I'm trying to trust the excerpt id of the selection head, but it's a sentinel value and not the actual excerpt id of the message. I think we probably need to resolve to offsets instead. --- crates/ai/src/assistant.rs | 93 +++++++++++++++++++++- crates/editor/src/selections_collection.rs | 3 + 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7020f9f38cc081ecc1baab6f2af318daf571fd0d..dcb658a0add8c63e84b3c3a96d835805a9d748e8 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -4,7 +4,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -545,6 +545,59 @@ impl Assistant { self.pending_completions.pop().is_some() } + fn remove_empty_messages<'a>( + &mut self, + protected: impl IntoIterator, + cx: &mut ModelContext, + ) { + dbg!(&self.messages_by_id); + + let protected = protected + .into_iter() + .filter_map(|excerpt_id| { + self.messages_by_id + .get(&excerpt_id) + .map(|message| message.content.id()) + }) + .collect::>(); + + dbg!(&protected); + + let empty = self + .messages_by_id + .values() + .filter_map(|message| { + if message.content.read(cx).is_empty() { + Some(message.content.id()) + } else { + None + } + }) + .collect::>(); + + dbg!(&empty); + + let mut remove_excerpts = Vec::new(); + self.messages.retain(|message| { + let is_empty = empty.contains(&message.content.id()); + let is_protected = dbg!(&protected).contains(&message.content.id()); + let retain_message = !is_empty || is_protected; + if !retain_message { + remove_excerpts.push(message.excerpt_id); + self.messages_by_id.remove(&message.excerpt_id); + } + retain_message + }); + + if !remove_excerpts.is_empty() { + self.buffer.update(cx, |buffer, cx| { + dbg!(buffer.excerpt_ids()); + buffer.remove_excerpts(dbg!(remove_excerpts), cx) + }); + cx.notify(); + } + } + fn push_message(&mut self, role: Role, cx: &mut ModelContext) -> Message { let content = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); @@ -578,6 +631,7 @@ impl Assistant { }); let message = Message { + excerpt_id, role, content: content.clone(), sent_at: Local::now(), @@ -657,8 +711,14 @@ impl AssistantEditor { ); editor }); + + let _subscriptions = vec![ + cx.observe(&assistant, |_, _, cx| cx.notify()), + cx.subscribe(&editor, Self::handle_editor_event), + ]; + Self { - _subscriptions: vec![cx.observe(&assistant, |_, _, cx| cx.notify())], + _subscriptions, assistant, editor, } @@ -678,6 +738,32 @@ impl AssistantEditor { } } + fn handle_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if *event == editor::Event::Edited { + self.remove_empty_messages(cx); + } + } + + // Removes empty messages that don't contain a cursor. + fn remove_empty_messages(&mut self, cx: &mut ViewContext) { + let anchored_selections = self.editor.read(cx).selections.disjoint_anchors(); + let protected_excerpts = anchored_selections + .iter() + .map(|selection| selection.head().excerpt_id()) + .collect::>(); + + dbg!(&protected_excerpts); + + self.assistant.update(cx, |assistant, cx| { + assistant.remove_empty_messages(protected_excerpts, cx) + }); + } + fn quote_selection( workspace: &mut Workspace, _: &QuoteSelection, @@ -804,8 +890,9 @@ impl Item for AssistantEditor { } } -#[derive(Clone)] +#[derive(Clone, Debug)] struct Message { + excerpt_id: ExcerptId, role: Role, content: ModelHandle, sent_at: DateTime, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 4c32b543b210b509b75319cb3094621116b557c8..d82ce5e21637fde58d1e19c866befe53270f2e9f 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -76,6 +76,9 @@ impl SelectionsCollection { count } + /// The non-pending, non-overlapping selections. There could still be a pending + /// selection that overlaps these if the mouse is being dragged, etc. Returned as + /// selections over Anchors. pub fn disjoint_anchors(&self) -> Arc<[Selection]> { self.disjoint.clone() } From e46d1549d6869bdce7476ee360ae644e91b1298f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 10:12:15 +0200 Subject: [PATCH 069/101] Retain selection's head (as opposed to its end) on insertion This makes a difference when an edit spans two excerpts and the selection start won't necessarily be the same as the selection end after the edit. --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 453468349be90a2b8f6165e6710a1ba23460d9ea..0eefa7e0985890dc75a7f060a440d26a4922cec1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2382,7 +2382,7 @@ impl Editor { old_selections .iter() .map(|s| { - let anchor = snapshot.anchor_after(s.end); + let anchor = snapshot.anchor_after(s.head()); s.map(|_| anchor) }) .collect::>() From 80323244707e3d934cd4d04f9074ee9618e0cd28 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 10:13:45 +0200 Subject: [PATCH 070/101] Prevent moving across excerpts on `Editor::delete` --- crates/editor/src/editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0eefa7e0985890dc75a7f060a440d26a4922cec1..9f2c0c86ff0a962c339423dd70204fb02e5df317 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3557,7 +3557,9 @@ impl Editor { s.move_with(|map, selection| { if selection.is_empty() && !line_mode { let cursor = movement::right(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); + selection.end = cursor; + selection.reversed = true; + selection.goal = SelectionGoal::None; } }) }); From 337dda8e3a8fd21aed3c6a9a482493f37bc063d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 12:25:09 +0200 Subject: [PATCH 071/101] Only remove excerpts when an edit touches them --- crates/ai/src/assistant.rs | 100 ++++++++++++------------------ crates/editor/src/editor.rs | 2 +- crates/editor/src/multi_buffer.rs | 11 ++++ 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index dcb658a0add8c63e84b3c3a96d835805a9d748e8..1141bcee8e5d8176d0eab3e22b06415ff2c15527 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -392,6 +392,10 @@ impl Panel for AssistantPanel { } } +enum AssistantEvent { + MessagesEdited { ids: Vec }, +} + struct Assistant { buffer: ModelHandle, messages: Vec, @@ -408,7 +412,7 @@ struct Assistant { } impl Entity for Assistant { - type Event = (); + type Event = AssistantEvent; } impl Assistant { @@ -448,6 +452,9 @@ impl Assistant { editor::multi_buffer::Event::ExcerptsAdded { .. } | editor::multi_buffer::Event::ExcerptsRemoved { .. } | editor::multi_buffer::Event::Edited => self.count_remaining_tokens(cx), + editor::multi_buffer::Event::ExcerptsEdited { ids } => { + cx.emit(AssistantEvent::MessagesEdited { ids: ids.clone() }); + } _ => {} } } @@ -547,52 +554,30 @@ impl Assistant { fn remove_empty_messages<'a>( &mut self, - protected: impl IntoIterator, + excerpts: HashSet, + protected_offsets: HashSet, cx: &mut ModelContext, ) { - dbg!(&self.messages_by_id); - - let protected = protected - .into_iter() - .filter_map(|excerpt_id| { - self.messages_by_id - .get(&excerpt_id) - .map(|message| message.content.id()) - }) - .collect::>(); - - dbg!(&protected); - - let empty = self - .messages_by_id - .values() - .filter_map(|message| { - if message.content.read(cx).is_empty() { - Some(message.content.id()) - } else { - None - } - }) - .collect::>(); - - dbg!(&empty); - - let mut remove_excerpts = Vec::new(); + let mut offset = 0; + let mut excerpts_to_remove = Vec::new(); self.messages.retain(|message| { - let is_empty = empty.contains(&message.content.id()); - let is_protected = dbg!(&protected).contains(&message.content.id()); - let retain_message = !is_empty || is_protected; - if !retain_message { - remove_excerpts.push(message.excerpt_id); + let range = offset..offset + message.content.read(cx).len(); + offset = range.end + 1; + if range.is_empty() + && !protected_offsets.contains(&range.start) + && excerpts.contains(&message.excerpt_id) + { + excerpts_to_remove.push(message.excerpt_id); self.messages_by_id.remove(&message.excerpt_id); + false + } else { + true } - retain_message }); - if !remove_excerpts.is_empty() { + if !excerpts_to_remove.is_empty() { self.buffer.update(cx, |buffer, cx| { - dbg!(buffer.excerpt_ids()); - buffer.remove_excerpts(dbg!(remove_excerpts), cx) + buffer.remove_excerpts(excerpts_to_remove, cx) }); cx.notify(); } @@ -714,7 +699,7 @@ impl AssistantEditor { let _subscriptions = vec![ cx.observe(&assistant, |_, _, cx| cx.notify()), - cx.subscribe(&editor, Self::handle_editor_event), + cx.subscribe(&assistant, Self::handle_assistant_event), ]; Self { @@ -738,32 +723,27 @@ impl AssistantEditor { } } - fn handle_editor_event( + fn handle_assistant_event( &mut self, - _: ViewHandle, - event: &editor::Event, + assistant: ModelHandle, + event: &AssistantEvent, cx: &mut ViewContext, ) { - if *event == editor::Event::Edited { - self.remove_empty_messages(cx); + match event { + AssistantEvent::MessagesEdited { ids } => { + let selections = self.editor.read(cx).selections.all::(cx); + let selection_heads = selections + .iter() + .map(|selection| selection.head()) + .collect::>(); + let ids = ids.iter().copied().collect::>(); + assistant.update(cx, |assistant, cx| { + assistant.remove_empty_messages(ids, selection_heads, cx) + }); + } } } - // Removes empty messages that don't contain a cursor. - fn remove_empty_messages(&mut self, cx: &mut ViewContext) { - let anchored_selections = self.editor.read(cx).selections.disjoint_anchors(); - let protected_excerpts = anchored_selections - .iter() - .map(|selection| selection.head().excerpt_id()) - .collect::>(); - - dbg!(&protected_excerpts); - - self.assistant.update(cx, |assistant, cx| { - assistant.remove_empty_messages(protected_excerpts, cx) - }); - } - fn quote_selection( workspace: &mut Workspace, _: &QuoteSelection, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9f2c0c86ff0a962c339423dd70204fb02e5df317..613536a95568f96a5c71e163c419e7708f36c687 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6907,7 +6907,7 @@ impl Editor { multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } - multi_buffer::Event::LanguageChanged => {} + _ => {} } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b7b2b89c8ca312667bfb3b5988da8292fd33c6bc..6011d1014234a394747f44bf7952333e219573cf 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -64,6 +64,9 @@ pub enum Event { ExcerptsRemoved { ids: Vec, }, + ExcerptsEdited { + ids: Vec, + }, Edited, Reloaded, DiffBaseChanged, @@ -387,6 +390,7 @@ impl MultiBuffer { original_indent_column: u32, } let mut buffer_edits: HashMap> = Default::default(); + let mut edited_excerpt_ids = Vec::new(); let mut cursor = snapshot.excerpts.cursor::(); for (ix, (range, new_text)) in edits.enumerate() { let new_text: Arc = new_text.into(); @@ -403,6 +407,7 @@ impl MultiBuffer { .start .to_offset(&start_excerpt.buffer) + start_overshoot; + edited_excerpt_ids.push(start_excerpt.id); cursor.seek(&range.end, Bias::Right, &()); if cursor.item().is_none() && range.end == *cursor.start() { @@ -428,6 +433,7 @@ impl MultiBuffer { original_indent_column, }); } else { + edited_excerpt_ids.push(end_excerpt.id); let start_excerpt_range = buffer_start ..start_excerpt .range @@ -474,6 +480,7 @@ impl MultiBuffer { is_insertion: false, original_indent_column, }); + edited_excerpt_ids.push(excerpt.id); cursor.next(&()); } } @@ -546,6 +553,10 @@ impl MultiBuffer { buffer.edit(insertions, insertion_autoindent_mode, cx); }) } + + cx.emit(Event::ExcerptsEdited { + ids: edited_excerpt_ids, + }); } pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { From f4f060667e588536510679b78c10882371ff0e11 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 12:36:26 +0200 Subject: [PATCH 072/101] Add assertion to pinpoint how deletion works across excerpts --- crates/editor/src/editor_tests.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bc671b9ffc3652856d16faf567784eab438440b7..df6459918e57b339b19c2faceca29dc9551e79d3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5225,7 +5225,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), ] - ) + ); + + // Ensure the cursor's head is respected when deleting across an excerpt boundary. + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "Xa\nbbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(1, 0)..Point::new(1, 0)] + ); + + view.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) + }); + view.backspace(&Default::default(), cx); + assert_eq!(view.text(cx), "X\nbb"); + assert_eq!( + view.selections.ranges(cx), + [Point::new(0, 1)..Point::new(0, 1)] + ); }); } From ada222078c7d83fe479fa0c51a9fdf749dbff17b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 12:43:08 +0200 Subject: [PATCH 073/101] Insert a user reply when hitting `cmd-enter` in an assistant message --- crates/ai/src/assistant.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1141bcee8e5d8176d0eab3e22b06415ff2c15527..6c3189b660822b7fbca5db5a0623a6c268889307 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -5,7 +5,7 @@ use crate::{ use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; -use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; +use editor::{Anchor, Editor, ExcerptId, ExcerptRange, MultiBuffer}; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ @@ -710,8 +710,25 @@ impl AssistantEditor { } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - self.assistant - .update(cx, |assistant, cx| assistant.assist(cx)); + self.assistant.update(cx, |assistant, cx| { + let editor = self.editor.read(cx); + let newest_selection = editor.selections.newest_anchor(); + let message = if newest_selection.head() == Anchor::min() { + assistant.messages.first() + } else if newest_selection.head() == Anchor::max() { + assistant.messages.last() + } else { + assistant + .messages_by_id + .get(&newest_selection.head().excerpt_id()) + }; + + if message.map_or(false, |message| message.role == Role::Assistant) { + assistant.push_message(Role::User, cx); + } else { + assistant.assist(cx); + } + }); } fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { From 69b8267b6bd6eec3b65e1f2b32e237bf0f828e3a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 13:04:52 +0200 Subject: [PATCH 074/101] Show the current model and allow clicking on it to change it --- crates/ai/src/assistant.rs | 85 +++++++++++++++++++++---------- crates/theme/src/theme.rs | 2 + styles/src/styleTree/assistant.ts | 26 ++++++++-- 3 files changed, 82 insertions(+), 31 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6c3189b660822b7fbca5db5a0623a6c268889307..d61dec9f72eab784654eeb29f375559e18dab4fe 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -9,15 +9,17 @@ use editor::{Anchor, Editor, ExcerptId, ExcerptRange, MultiBuffer}; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ - actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity, - ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + actions, + elements::*, + executor::Background, + platform::{CursorStyle, MouseButton}, + Action, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, + View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; use std::{cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; -use tiktoken_rs::model::get_context_size; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -430,7 +432,7 @@ impl Assistant { pending_completions: Default::default(), languages: language_registry, token_count: None, - max_token_count: get_context_size(model), + max_token_count: tiktoken_rs::model::get_context_size(model), pending_token_count: Task::ready(None), model: model.into(), _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], @@ -483,6 +485,7 @@ impl Assistant { .await?; this.update(&mut cx, |this, cx| { + this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); this.token_count = Some(token_count); cx.notify() }); @@ -496,6 +499,12 @@ impl Assistant { Some(self.max_token_count as isize - self.token_count? as isize) } + fn set_model(&mut self, model: String, cx: &mut ModelContext) { + self.model = model; + self.count_remaining_tokens(cx); + cx.notify(); + } + fn assist(&mut self, cx: &mut ModelContext) { let messages = self .messages @@ -825,6 +834,16 @@ impl AssistantEditor { }); } } + + fn cycle_model(&mut self, cx: &mut ViewContext) { + self.assistant.update(cx, |assistant, cx| { + let new_model = match assistant.model.as_str() { + "gpt-4" => "gpt-3.5-turbo", + _ => "gpt-4", + }; + assistant.set_model(new_model.into(), cx); + }); + } } impl Entity for AssistantEditor { @@ -837,27 +856,23 @@ impl View for AssistantEditor { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum Model {} let theme = &theme::current(cx).assistant; - let remaining_tokens = self - .assistant - .read(cx) - .remaining_tokens() - .map(|remaining_tokens| { - let remaining_tokens_style = if remaining_tokens <= 0 { - &theme.no_remaining_tokens - } else { - &theme.remaining_tokens - }; - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container) - .aligned() - .top() - .right() - }); + let assistant = &self.assistant.read(cx); + let model = assistant.model.clone(); + let remaining_tokens = assistant.remaining_tokens().map(|remaining_tokens| { + let remaining_tokens_style = if remaining_tokens <= 0 { + &theme.no_remaining_tokens + } else { + &theme.remaining_tokens + }; + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container) + }); Stack::new() .with_child( @@ -865,7 +880,25 @@ impl View for AssistantEditor { .contained() .with_style(theme.container), ) - .with_children(remaining_tokens) + .with_child( + Flex::row() + .with_child( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.model.style_for(state, false); + Label::new(model, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)), + ) + .with_children(remaining_tokens) + .contained() + .with_style(theme.model_info_container) + .aligned() + .top() + .right(), + ) .into_any() } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 97aac92afd153d7c7bb4267ac8e5ca445ada7a10..f746f901939427e144b1c1dabbce697c9c08a4d5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -976,6 +976,8 @@ pub struct AssistantStyle { pub sent_at: ContainedText, pub user_sender: ContainedText, pub assistant_sender: ContainedText, + pub model_info_container: ContainerStyle, + pub model: Interactive, pub remaining_tokens: ContainedText, pub no_remaining_tokens: ContainedText, pub api_key_editor: FieldEditor, diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 3d21ee8519f3eeb30e8437e07f69eca5f72c8597..217476bc31a6b4b2813f64fde7393483b445d8a5 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -11,7 +11,8 @@ export default function assistant(colorScheme: ColorScheme) { }, header: { border: border(layer, "default", { bottom: true, top: true }), - margin: { bottom: 6, top: 6 } + margin: { bottom: 6, top: 6 }, + background: editor(colorScheme).background }, user_sender: { ...text(layer, "sans", "default", { size: "sm", weight: "bold" }), @@ -23,17 +24,32 @@ export default function assistant(colorScheme: ColorScheme) { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), }, - remaining_tokens: { - padding: 4, + model_info_container: { margin: { right: 16, top: 4 }, + }, + model: { background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + cornerRadius: 4, + ...text(layer, "sans", "default", { size: "xs" }), + hover: { + background: background(layer, "on", "hovered"), + } + }, + remaining_tokens: { + background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + margin: { left: 4 }, cornerRadius: 4, ...text(layer, "sans", "positive", { size: "xs" }), }, no_remaining_tokens: { - padding: 4, - margin: { right: 16, top: 4 }, background: background(layer, "on"), + border: border(layer, "on", { overlay: true }), + padding: 4, + margin: { left: 4 }, cornerRadius: 4, ...text(layer, "sans", "negative", { size: "xs" }), }, From 9c59146026eedc3fa5fa5fa2d4121f47eaec9eb5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 15:59:10 +0200 Subject: [PATCH 075/101] Set assistant editor's title based on the first question/answer pair Co-Authored-By: Julia Risley --- assets/keymaps/default.json | 2 +- crates/ai/src/assistant.rs | 119 +++++++++++++++++++++++++++++++---- crates/workspace/src/pane.rs | 3 +- 3 files changed, 111 insertions(+), 13 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 41087326d27afe9c80a4fc0a27e0b3e31ca8cbf2..71c34da20129d11cacc6bd047fbc871b9ad81479 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -197,7 +197,7 @@ } }, { - "context": "ContextEditor > Editor", + "context": "AssistantEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", "cmd->": "assistant::QuoteSelection" diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index d61dec9f72eab784654eeb29f375559e18dab4fe..0f7ca509d449104ffc63f10aacc248921bdd6821 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,8 +19,8 @@ use gpui::{ use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; -use std::{cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; -use util::{post_inc, ResultExt, TryFutureExt}; +use std::{borrow::Cow, cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; +use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::Item, @@ -69,7 +69,7 @@ pub struct AssistantPanel { has_read_credentials: bool, languages: Arc, fs: Arc, - _subscriptions: Vec, + subscriptions: Vec, } impl AssistantPanel { @@ -145,11 +145,11 @@ impl AssistantPanel { fs: workspace.app_state().fs.clone(), width: None, height: None, - _subscriptions: Default::default(), + subscriptions: Default::default(), }; let mut old_dock_position = this.position(cx); - this._subscriptions = vec![ + this.subscriptions = vec![ cx.observe(&this.pane, |_, _, cx| cx.notify()), cx.subscribe(&this.pane, Self::handle_pane_event), cx.observe_global::(move |this, cx| { @@ -186,11 +186,24 @@ impl AssistantPanel { let focus = self.has_focus(cx); let editor = cx .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(editor), true, focus, None, cx) }); } + fn handle_assistant_editor_event( + &mut self, + _: ViewHandle, + event: &AssistantEditorEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()), + } + } + fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { if let Some(api_key) = self .api_key_editor @@ -396,12 +409,15 @@ impl Panel for AssistantPanel { enum AssistantEvent { MessagesEdited { ids: Vec }, + SummaryChanged, } struct Assistant { buffer: ModelHandle, messages: Vec, messages_by_id: HashMap, + summary: Option, + pending_summary: Task>, completion_count: usize, pending_completions: Vec, languages: Arc, @@ -428,6 +444,8 @@ impl Assistant { let mut this = Self { messages: Default::default(), messages_by_id: Default::default(), + summary: None, + pending_summary: Task::ready(None), completion_count: Default::default(), pending_completions: Default::default(), languages: language_registry, @@ -540,9 +558,10 @@ impl Assistant { } } - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { this.pending_completions .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); }); anyhow::Ok(()) @@ -634,6 +653,54 @@ impl Assistant { self.messages_by_id.insert(excerpt_id, message.clone()); message } + + fn summarize(&mut self, cx: &mut ModelContext) { + if self.messages.len() >= 2 && self.summary.is_none() { + let api_key = self.api_key.borrow().clone(); + if let Some(api_key) = api_key { + let messages = self + .messages + .iter() + .take(2) + .map(|message| RequestMessage { + role: message.role, + content: message.content.read(cx).text(), + }) + .chain(Some(RequestMessage { + role: Role::User, + content: "Summarize the conversation into a short title without punctuation and with as few characters as possible" + .into(), + })) + .collect(); + let request = OpenAIRequest { + model: self.model.clone(), + messages, + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + let text = choice.delta.content.unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.summary.get_or_insert(String::new()).push_str(&text); + cx.emit(AssistantEvent::SummaryChanged); + }); + } + } + + anyhow::Ok(()) + } + .log_err() + }); + } + } + } } struct PendingCompletion { @@ -641,6 +708,10 @@ struct PendingCompletion { _task: Task>, } +enum AssistantEditorEvent { + TabContentChanged, +} + struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, @@ -712,9 +783,9 @@ impl AssistantEditor { ]; Self { - _subscriptions, assistant, editor, + _subscriptions, } } @@ -767,6 +838,9 @@ impl AssistantEditor { assistant.remove_empty_messages(ids, selection_heads, cx) }); } + AssistantEvent::SummaryChanged => { + cx.emit(AssistantEditorEvent::TabContentChanged); + } } } @@ -844,15 +918,23 @@ impl AssistantEditor { assistant.set_model(new_model.into(), cx); }); } + + fn title(&self, cx: &AppContext) -> String { + self.assistant + .read(cx) + .summary + .clone() + .unwrap_or_else(|| "New Context".into()) + } } impl Entity for AssistantEditor { - type Event = (); + type Event = AssistantEditorEvent; } impl View for AssistantEditor { fn ui_name() -> &'static str { - "ContextEditor" + "AssistantEditor" } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { @@ -914,9 +996,14 @@ impl Item for AssistantEditor { &self, _: Option, style: &theme::Tab, - _: &gpui::AppContext, + cx: &gpui::AppContext, ) -> AnyElement { - Label::new("New Context", style.label.clone()).into_any() + let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN); + Label::new(title, style.label.clone()).into_any() + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + Some(self.title(cx).into()) } } @@ -964,9 +1051,19 @@ async fn stream_completion( while let Some(line) = lines.next().await { if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); if tx.unbounded_send(event).is_err() { break; } + + if done { + break; + } } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1fa8c15f9bc1f5399fbcda51cb68d2b594543c46..ca5e4e981cef76802f15408d0f71c88e6101b1e1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1151,7 +1151,8 @@ impl Pane { let theme = theme::current(cx).clone(); let mut tooltip_theme = theme.tooltip.clone(); tooltip_theme.max_text_width = None; - let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); + let tab_tooltip_text = + item.tab_tooltip_text(cx).map(|text| text.into_owned()); move |mouse_state, cx| { let tab_style = From 2b1aeb07bc7acd378af090e403431585943139e4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 16:53:56 +0200 Subject: [PATCH 076/101] Show error message when requests to OpenAI fail Co-Authored-By: Julia Risley --- crates/ai/src/assistant.rs | 124 ++++++++++++++++++++++-------- crates/theme/src/theme.rs | 1 + styles/src/styleTree/assistant.ts | 19 +++-- 3 files changed, 106 insertions(+), 38 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 0f7ca509d449104ffc63f10aacc248921bdd6821..b70ed8c87cf657e80fe5dd2c9c80e0372e9687af 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -18,6 +18,7 @@ use gpui::{ }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; +use serde::Deserialize; use settings::SettingsStore; use std::{borrow::Cow, cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; @@ -415,7 +416,7 @@ enum AssistantEvent { struct Assistant { buffer: ModelHandle, messages: Vec, - messages_by_id: HashMap, + messages_metadata: HashMap, summary: Option, pending_summary: Task>, completion_count: usize, @@ -443,7 +444,7 @@ impl Assistant { let buffer = cx.add_model(|_| MultiBuffer::new(0)); let mut this = Self { messages: Default::default(), - messages_by_id: Default::default(), + messages_metadata: Default::default(), summary: None, pending_summary: Task::ready(None), completion_count: Default::default(), @@ -541,16 +542,16 @@ impl Assistant { let api_key = self.api_key.borrow().clone(); if let Some(api_key) = api_key { let stream = stream_completion(api_key, cx.background().clone(), request); - let response = self.push_message(Role::Assistant, cx); + let (excerpt_id, content) = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); - let task = cx.spawn(|this, mut cx| { - async move { + let task = cx.spawn(|this, mut cx| async move { + let stream_completion = async { let mut messages = stream.await?; while let Some(message) = messages.next().await { let mut message = message?; if let Some(choice) = message.choices.pop() { - response.content.update(&mut cx, |content, cx| { + content.update(&mut cx, |content, cx| { let text: Arc = choice.delta.content?.into(); content.edit([(content.len()..content.len(), text)], None, cx); Some(()) @@ -565,8 +566,16 @@ impl Assistant { }); anyhow::Ok(()) + }; + + if let Err(error) = stream_completion.await { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = this.messages_metadata.get_mut(&excerpt_id) { + metadata.error = Some(error.to_string().trim().into()); + cx.notify(); + } + }) } - .log_err() }); self.pending_completions.push(PendingCompletion { @@ -596,7 +605,7 @@ impl Assistant { && excerpts.contains(&message.excerpt_id) { excerpts_to_remove.push(message.excerpt_id); - self.messages_by_id.remove(&message.excerpt_id); + self.messages_metadata.remove(&message.excerpt_id); false } else { true @@ -611,7 +620,11 @@ impl Assistant { } } - fn push_message(&mut self, role: Role, cx: &mut ModelContext) -> Message { + fn push_message( + &mut self, + role: Role, + cx: &mut ModelContext, + ) -> (ExcerptId, ModelHandle) { let content = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); let markdown = self.languages.language_for_name("Markdown"); @@ -643,15 +656,20 @@ impl Assistant { .unwrap() }); - let message = Message { + self.messages.push(Message { excerpt_id, role, content: content.clone(), - sent_at: Local::now(), - }; - self.messages.push(message.clone()); - self.messages_by_id.insert(excerpt_id, message.clone()); - message + }); + self.messages_metadata.insert( + excerpt_id, + MessageMetadata { + role, + sent_at: Local::now(), + error: None, + }, + ); + (excerpt_id, content) } fn summarize(&mut self, cx: &mut ModelContext) { @@ -705,7 +723,7 @@ impl Assistant { struct PendingCompletion { id: usize, - _task: Task>, + _task: Task<()>, } enum AssistantEditorEvent { @@ -733,9 +751,13 @@ impl AssistantEditor { { let assistant = assistant.clone(); move |_editor, params: editor::RenderExcerptHeaderParams, cx| { - let style = &theme::current(cx).assistant; - if let Some(message) = assistant.read(cx).messages_by_id.get(¶ms.id) { - let sender = match message.role { + enum ErrorTooltip {} + + let theme = theme::current(cx); + let style = &theme.assistant; + if let Some(metadata) = assistant.read(cx).messages_metadata.get(¶ms.id) + { + let sender = match metadata.role { Role::User => Label::new("You", style.user_sender.text.clone()) .contained() .with_style(style.user_sender.container), @@ -755,13 +777,29 @@ impl AssistantEditor { .with_child(sender.aligned()) .with_child( Label::new( - message.sent_at.format("%I:%M%P").to_string(), + metadata.sent_at.format("%I:%M%P").to_string(), style.sent_at.text.clone(), ) .contained() .with_style(style.sent_at.container) .aligned(), ) + .with_children(metadata.error.clone().map(|error| { + Svg::new("icons/circle_x_mark_12.svg") + .with_color(style.error_icon.color) + .constrained() + .with_width(style.error_icon.width) + .contained() + .with_style(style.error_icon.container) + .with_tooltip::( + params.id.into(), + error, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + })) .aligned() .left() .contained() @@ -793,17 +831,18 @@ impl AssistantEditor { self.assistant.update(cx, |assistant, cx| { let editor = self.editor.read(cx); let newest_selection = editor.selections.newest_anchor(); - let message = if newest_selection.head() == Anchor::min() { - assistant.messages.first() + let role = if newest_selection.head() == Anchor::min() { + assistant.messages.first().map(|message| message.role) } else if newest_selection.head() == Anchor::max() { - assistant.messages.last() + assistant.messages.last().map(|message| message.role) } else { assistant - .messages_by_id + .messages_metadata .get(&newest_selection.head().excerpt_id()) + .map(|message| message.role) }; - if message.map_or(false, |message| message.role == Role::Assistant) { + if role.map_or(false, |role| role == Role::Assistant) { assistant.push_message(Role::User, cx); } else { assistant.assist(cx); @@ -1007,12 +1046,18 @@ impl Item for AssistantEditor { } } -#[derive(Clone, Debug)] +#[derive(Debug)] struct Message { excerpt_id: ExcerptId, role: Role, content: ModelHandle, +} + +#[derive(Debug)] +struct MessageMetadata { + role: Role, sent_at: DateTime, + error: Option, } async fn stream_completion( @@ -1076,10 +1121,27 @@ async fn stream_completion( let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; - Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )) + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f746f901939427e144b1c1dabbce697c9c08a4d5..132e37ad1c830155f3713c92d0e224ddbfdc214a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -980,6 +980,7 @@ pub struct AssistantStyle { pub model: Interactive, pub remaining_tokens: ContainedText, pub no_remaining_tokens: ContainedText, + pub error_icon: Icon, pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, } diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 217476bc31a6b4b2813f64fde7393483b445d8a5..4314741fb0b566b5abbc64eaad4af8afad55986c 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { text, border, background } from "./components" +import { text, border, background, foreground } from "./components" import editor from "./editor" export default function assistant(colorScheme: ColorScheme) { @@ -14,17 +14,17 @@ export default function assistant(colorScheme: ColorScheme) { margin: { bottom: 6, top: 6 }, background: editor(colorScheme).background }, - user_sender: { + userSender: { ...text(layer, "sans", "default", { size: "sm", weight: "bold" }), }, - assistant_sender: { + assistantSender: { ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), }, - sent_at: { + sentAt: { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), }, - model_info_container: { + modelInfoContainer: { margin: { right: 16, top: 4 }, }, model: { @@ -37,7 +37,7 @@ export default function assistant(colorScheme: ColorScheme) { background: background(layer, "on", "hovered"), } }, - remaining_tokens: { + remainingTokens: { background: background(layer, "on"), border: border(layer, "on", { overlay: true }), padding: 4, @@ -45,7 +45,7 @@ export default function assistant(colorScheme: ColorScheme) { cornerRadius: 4, ...text(layer, "sans", "positive", { size: "xs" }), }, - no_remaining_tokens: { + noRemainingTokens: { background: background(layer, "on"), border: border(layer, "on", { overlay: true }), padding: 4, @@ -53,6 +53,11 @@ export default function assistant(colorScheme: ColorScheme) { cornerRadius: 4, ...text(layer, "sans", "negative", { size: "xs" }), }, + errorIcon: { + margin: { left: 8 }, + color: foreground(layer, "negative"), + width: 12, + }, apiKeyEditor: { background: background(layer, "on"), cornerRadius: 6, From a0e2e5db7dca2157d2693d91e98052c1351ca5f9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:55:38 +0200 Subject: [PATCH 077/101] project panel/styles: Align child's chevron with parent path (#2559) Z-1012 Release notes: - Adjust indent of files in subdirectories. --- styles/src/styleTree/projectPanel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 08117bf6b01d5fe7b14f2e0ace22dc680c5cefac..666d236da674690f8d2fcd9073165ab82484b307 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -72,7 +72,7 @@ export default function projectPanel(colorScheme: ColorScheme) { }, background: background(layer), padding: { left: 12, right: 12, top: 6, bottom: 6 }, - indentWidth: 8, + indentWidth: 16, entry, draggedEntry: { ...baseEntry, From 7b066df7e6a03c66b2f393d2b630ce939a1d6b00 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 6 Jun 2023 11:31:53 -0400 Subject: [PATCH 078/101] Tighten up spacing in the project panel (#2574) Following https://github.com/zed-industries/zed/pull/2559 the project panel entries become pretty wide again. This PR tries to mitigate that and just make some general improvements to visual density in the project panel. - Reduces padding around items - Removes top margin - Slightly reduces the height of each item - Fixes an issue where ignored files had the wrong color chevron Release Notes: - Improved density of the project panel and tidied up some visual issues. --- styles/src/styleTree/projectPanel.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 666d236da674690f8d2fcd9073165ab82484b307..d2cc1294526470f33d75cee3da4cbdb52eee00e0 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -8,10 +8,10 @@ export default function projectPanel(colorScheme: ColorScheme) { let layer = colorScheme.middle let baseEntry = { - height: 24, + height: 22, iconColor: foreground(layer, "variant"), - iconSize: 8, - iconSpacing: 8, + iconSize: 7, + iconSpacing: 5, } let status = { @@ -71,8 +71,8 @@ export default function projectPanel(colorScheme: ColorScheme) { }, }, background: background(layer), - padding: { left: 12, right: 12, top: 6, bottom: 6 }, - indentWidth: 16, + padding: { left: 6, right: 6, top: 0, bottom: 6 }, + indentWidth: 12, entry, draggedEntry: { ...baseEntry, @@ -83,7 +83,12 @@ export default function projectPanel(colorScheme: ColorScheme) { }, ignoredEntry: { ...entry, + iconColor: foreground(layer, "disabled"), text: text(layer, "mono", "disabled"), + active: { + ...entry.active, + iconColor: foreground(layer, "variant"), + } }, cutEntry: { ...entry, From 093ce8a9ac6e20ebfdde2786733f3eff8d571fad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 17:45:58 +0200 Subject: [PATCH 079/101] Simplify prompt Co-Authored-By: Nathan Sobo --- crates/ai/src/assistant.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index b70ed8c87cf657e80fe5dd2c9c80e0372e9687af..f505ea1f3fe4e99e621549c9e174bac98c812000 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -686,8 +686,9 @@ impl Assistant { }) .chain(Some(RequestMessage { role: Role::User, - content: "Summarize the conversation into a short title without punctuation and with as few characters as possible" - .into(), + content: + "Summarize the conversation into a short title without punctuation" + .into(), })) .collect(); let request = OpenAIRequest { From cfcfc3bf6b1493e7668ed6a0c433f198690d6358 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 09:03:57 -0700 Subject: [PATCH 080/101] Show notification when attempting to open local settings in a project w/ no folders --- crates/zed/src/zed.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f0e88133f3e20c668b2c9c86108ed3abfe1f712e..8d27a378c5c49d6ebf3d7de3b043e3a908423420 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -44,8 +44,9 @@ use uuid::Uuid; use welcome::BaseKeymap; pub use workspace; use workspace::{ - create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow, - Workspace, WorkspaceSettings, + create_and_open_local_file, dock::PanelHandle, + notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, + NewWindow, Workspace, WorkspaceSettings, }; #[derive(Deserialize, Clone, PartialEq)] @@ -626,6 +627,10 @@ fn open_local_settings_file( anyhow::Ok(()) }) .detach(); + } else { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_| MessageNotification::new("This project has no folders open.")) + }) } } From ac7178068fd06bf000525e9d3071e59c36d7de91 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 18:18:04 +0200 Subject: [PATCH 081/101] Include message headers in copied assistant text Co-Authored-By: Nathan Sobo --- crates/ai/src/ai.rs | 11 +++++++++ crates/ai/src/assistant.rs | 46 +++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 11704de03e6c8b5ecfb2d944e68bf545b45da742..6f26f00c52e19620a3be150cb89df8b4223b8200 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -4,6 +4,7 @@ mod assistant_settings; pub use assistant::AssistantPanel; use gpui::AppContext; use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; // Data types for chat completion requests #[derive(Serialize)] @@ -33,6 +34,16 @@ enum Role { System, } +impl Display for Role { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Role::User => write!(f, "User"), + Role::Assistant => write!(f, "Assistant"), + Role::System => write!(f, "System"), + } + } +} + #[derive(Deserialize, Debug)] struct OpenAIResponseStreamEvent { pub id: Option, diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f505ea1f3fe4e99e621549c9e174bac98c812000..a61ecf202d5737c0f718878dab70b9702dd44101 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -13,14 +13,14 @@ use gpui::{ elements::*, executor::Background, platform::{CursorStyle, MouseButton}, - Action, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, - View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use serde::Deserialize; use settings::SettingsStore; -use std::{borrow::Cow, cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; +use std::{borrow::Cow, cell::RefCell, cmp, fmt::Write, io, rc::Rc, sync::Arc, time::Duration}; use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -49,6 +49,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); cx.add_action(AssistantEditor::quote_selection); + cx.capture_action(AssistantEditor::copy); cx.add_action(AssistantPanel::save_api_key); cx.add_action(AssistantPanel::reset_api_key); } @@ -949,6 +950,45 @@ impl AssistantEditor { } } + fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { + let editor = self.editor.read(cx); + let assistant = self.assistant.read(cx); + if editor.selections.count() == 1 { + let selection = editor.selections.newest::(cx); + let mut offset = 0; + let mut copied_text = String::new(); + let mut spanned_messages = 0; + for message in &assistant.messages { + let message_range = offset..offset + message.content.read(cx).len() + 1; + + if message_range.start >= selection.range().end { + break; + } else if message_range.end >= selection.range().start { + let range = cmp::max(message_range.start, selection.range().start) + ..cmp::min(message_range.end, selection.range().end); + if !range.is_empty() { + spanned_messages += 1; + write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); + for chunk in assistant.buffer.read(cx).snapshot(cx).text_for_range(range) { + copied_text.push_str(&chunk); + } + copied_text.push('\n'); + } + } + + offset = message_range.end; + } + + if spanned_messages > 1 { + cx.platform() + .write_to_clipboard(ClipboardItem::new(copied_text)); + return; + } + } + + cx.propagate_action(); + } + fn cycle_model(&mut self, cx: &mut ViewContext) { self.assistant.update(cx, |assistant, cx| { let new_model = match assistant.model.as_str() { From ef7ec265c8c288ab3c0d83c0c31d84815b17365d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 18:45:08 +0200 Subject: [PATCH 082/101] Cycle message roles on click Co-Authored-By: Nathan Sobo --- crates/ai/src/ai.rs | 10 +++ crates/ai/src/assistant.rs | 139 +++++++++++++++++++----------- crates/theme/src/theme.rs | 5 +- styles/src/styleTree/assistant.ts | 3 + 4 files changed, 106 insertions(+), 51 deletions(-) diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 6f26f00c52e19620a3be150cb89df8b4223b8200..40224b3229de1665e3fac89be0d035154e2cf67f 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -34,6 +34,16 @@ enum Role { System, } +impl Role { + pub fn cycle(&mut self) { + *self = match self { + Role::User => Role::Assistant, + Role::Assistant => Role::System, + Role::System => Role::User, + } + } +} + impl Display for Role { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index a61ecf202d5737c0f718878dab70b9702dd44101..4a8319015fd669150019b8caab408e529a17304c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -485,14 +485,16 @@ impl Assistant { let messages = self .messages .iter() - .map(|message| tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: message.content.read(cx).text(), - name: None, + .filter_map(|message| { + Some(tiktoken_rs::ChatCompletionRequestMessage { + role: match self.messages_metadata.get(&message.excerpt_id)?.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: message.content.read(cx).text(), + name: None, + }) }) .collect::>(); let model = self.model.clone(); @@ -529,9 +531,11 @@ impl Assistant { let messages = self .messages .iter() - .map(|message| RequestMessage { - role: message.role, - content: message.content.read(cx).text(), + .filter_map(|message| { + Some(RequestMessage { + role: self.messages_metadata.get(&message.excerpt_id)?.role, + content: message.content.read(cx).text(), + }) }) .collect(); let request = OpenAIRequest { @@ -621,6 +625,13 @@ impl Assistant { } } + fn cycle_message_role(&mut self, excerpt_id: ExcerptId, cx: &mut ModelContext) { + if let Some(metadata) = self.messages_metadata.get_mut(&excerpt_id) { + metadata.role.cycle(); + cx.notify(); + } + } + fn push_message( &mut self, role: Role, @@ -659,7 +670,6 @@ impl Assistant { self.messages.push(Message { excerpt_id, - role, content: content.clone(), }); self.messages_metadata.insert( @@ -681,9 +691,11 @@ impl Assistant { .messages .iter() .take(2) - .map(|message| RequestMessage { - role: message.role, - content: message.content.read(cx).text(), + .filter_map(|message| { + Some(RequestMessage { + role: self.messages_metadata.get(&message.excerpt_id)?.role, + content: message.content.read(cx).text(), + }) }) .chain(Some(RequestMessage { role: Role::User, @@ -753,27 +765,51 @@ impl AssistantEditor { { let assistant = assistant.clone(); move |_editor, params: editor::RenderExcerptHeaderParams, cx| { + enum Sender {} enum ErrorTooltip {} let theme = theme::current(cx); let style = &theme.assistant; - if let Some(metadata) = assistant.read(cx).messages_metadata.get(¶ms.id) + let excerpt_id = params.id; + if let Some(metadata) = assistant + .read(cx) + .messages_metadata + .get(&excerpt_id) + .cloned() { - let sender = match metadata.role { - Role::User => Label::new("You", style.user_sender.text.clone()) - .contained() - .with_style(style.user_sender.container), - Role::Assistant => { - Label::new("Assistant", style.assistant_sender.text.clone()) - .contained() - .with_style(style.assistant_sender.container) + let sender = MouseEventHandler::::new( + params.id.into(), + cx, + |state, _| match metadata.role { + Role::User => { + let style = style.user_sender.style_for(state, false); + Label::new("You", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::Assistant => { + let style = style.assistant_sender.style_for(state, false); + Label::new("Assistant", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::System => { + let style = style.system_sender.style_for(state, false); + Label::new("System", style.text.clone()) + .contained() + .with_style(style.container) + } + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, { + let assistant = assistant.clone(); + move |_, _, cx| { + assistant.update(cx, |assistant, cx| { + assistant.cycle_message_role(excerpt_id, cx) + }) } - Role::System => { - Label::new("System", style.assistant_sender.text.clone()) - .contained() - .with_style(style.assistant_sender.container) - } - }; + }); Flex::row() .with_child(sender.aligned()) @@ -786,7 +822,7 @@ impl AssistantEditor { .with_style(style.sent_at.container) .aligned(), ) - .with_children(metadata.error.clone().map(|error| { + .with_children(metadata.error.map(|error| { Svg::new("icons/circle_x_mark_12.svg") .with_color(style.error_icon.color) .constrained() @@ -833,21 +869,22 @@ impl AssistantEditor { self.assistant.update(cx, |assistant, cx| { let editor = self.editor.read(cx); let newest_selection = editor.selections.newest_anchor(); - let role = if newest_selection.head() == Anchor::min() { - assistant.messages.first().map(|message| message.role) + let excerpt_id = if newest_selection.head() == Anchor::min() { + assistant.messages.first().map(|message| message.excerpt_id) } else if newest_selection.head() == Anchor::max() { - assistant.messages.last().map(|message| message.role) + assistant.messages.last().map(|message| message.excerpt_id) } else { - assistant - .messages_metadata - .get(&newest_selection.head().excerpt_id()) - .map(|message| message.role) + Some(newest_selection.head().excerpt_id()) }; - if role.map_or(false, |role| role == Role::Assistant) { - assistant.push_message(Role::User, cx); - } else { - assistant.assist(cx); + if let Some(excerpt_id) = excerpt_id { + if let Some(metadata) = assistant.messages_metadata.get(&excerpt_id) { + if metadata.role == Role::User { + assistant.assist(cx); + } else { + assistant.push_message(Role::User, cx); + } + } } }); } @@ -967,12 +1004,17 @@ impl AssistantEditor { let range = cmp::max(message_range.start, selection.range().start) ..cmp::min(message_range.end, selection.range().end); if !range.is_empty() { - spanned_messages += 1; - write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); - for chunk in assistant.buffer.read(cx).snapshot(cx).text_for_range(range) { - copied_text.push_str(&chunk); + if let Some(metadata) = assistant.messages_metadata.get(&message.excerpt_id) + { + spanned_messages += 1; + write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap(); + for chunk in + assistant.buffer.read(cx).snapshot(cx).text_for_range(range) + { + copied_text.push_str(&chunk); + } + copied_text.push('\n'); } - copied_text.push('\n'); } } @@ -1090,11 +1132,10 @@ impl Item for AssistantEditor { #[derive(Debug)] struct Message { excerpt_id: ExcerptId, - role: Role, content: ModelHandle, } -#[derive(Debug)] +#[derive(Clone, Debug)] struct MessageMetadata { role: Role, sent_at: DateTime, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 132e37ad1c830155f3713c92d0e224ddbfdc214a..f7df63ca099d9a50cf39c565d9cb658aafe098a1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -974,8 +974,9 @@ pub struct AssistantStyle { pub container: ContainerStyle, pub header: ContainerStyle, pub sent_at: ContainedText, - pub user_sender: ContainedText, - pub assistant_sender: ContainedText, + pub user_sender: Interactive, + pub assistant_sender: Interactive, + pub system_sender: Interactive, pub model_info_container: ContainerStyle, pub model: Interactive, pub remaining_tokens: ContainedText, diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts index 4314741fb0b566b5abbc64eaad4af8afad55986c..5e33967b50c30975184ead9094f75d2b3fdbe71d 100644 --- a/styles/src/styleTree/assistant.ts +++ b/styles/src/styleTree/assistant.ts @@ -20,6 +20,9 @@ export default function assistant(colorScheme: ColorScheme) { assistantSender: { ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), }, + systemSender: { + ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }), + }, sentAt: { margin: { top: 2, left: 8 }, ...text(layer, "sans", "default", { size: "2xs" }), From 16090c35ae02d3af6bf226ca4de2fe21804a6de0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Jun 2023 19:15:06 +0200 Subject: [PATCH 083/101] Insert reply after assistant message when hitting `cmd-enter` Co-Authored-By: Nathan Sobo --- crates/ai/src/assistant.rs | 104 ++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4a8319015fd669150019b8caab408e529a17304c..7f387194b35634f6364869d9798ce76ef5567b37 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -459,7 +459,7 @@ impl Assistant { api_key, buffer, }; - this.push_message(Role::User, cx); + this.insert_message_after(ExcerptId::max(), Role::User, cx); this.count_remaining_tokens(cx); this } @@ -498,7 +498,7 @@ impl Assistant { }) .collect::>(); let model = self.model.clone(); - self.pending_token_count = cx.spawn(|this, mut cx| { + self.pending_token_count = cx.spawn_weak(|this, mut cx| { async move { cx.background().timer(Duration::from_millis(200)).await; let token_count = cx @@ -506,11 +506,13 @@ impl Assistant { .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) }) .await?; - this.update(&mut cx, |this, cx| { - this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); - this.token_count = Some(token_count); - cx.notify() - }); + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |this, cx| { + this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); + this.token_count = Some(token_count); + cx.notify() + }); anyhow::Ok(()) } .log_err() @@ -547,9 +549,10 @@ impl Assistant { let api_key = self.api_key.borrow().clone(); if let Some(api_key) = api_key { let stream = stream_completion(api_key, cx.background().clone(), request); - let (excerpt_id, content) = self.push_message(Role::Assistant, cx); - self.push_message(Role::User, cx); - let task = cx.spawn(|this, mut cx| async move { + let (excerpt_id, content) = + self.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + self.insert_message_after(ExcerptId::max(), Role::User, cx); + let task = cx.spawn_weak(|this, mut cx| async move { let stream_completion = async { let mut messages = stream.await?; @@ -564,22 +567,26 @@ impl Assistant { } } - this.update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); anyhow::Ok(()) }; if let Err(error) = stream_completion.await { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = this.messages_metadata.get_mut(&excerpt_id) { - metadata.error = Some(error.to_string().trim().into()); - cx.notify(); - } - }) + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = this.messages_metadata.get_mut(&excerpt_id) { + metadata.error = Some(error.to_string().trim().into()); + cx.notify(); + } + }); + } } }); @@ -632,8 +639,9 @@ impl Assistant { } } - fn push_message( + fn insert_message_after( &mut self, + excerpt_id: ExcerptId, role: Role, cx: &mut ModelContext, ) -> (ExcerptId, ModelHandle) { @@ -654,9 +662,10 @@ impl Assistant { buffer.set_language_registry(self.languages.clone()); buffer }); - let excerpt_id = self.buffer.update(cx, |buffer, cx| { + let new_excerpt_id = self.buffer.update(cx, |buffer, cx| { buffer - .push_excerpts( + .insert_excerpts_after( + excerpt_id, content.clone(), vec![ExcerptRange { context: 0..0, @@ -668,19 +677,27 @@ impl Assistant { .unwrap() }); - self.messages.push(Message { - excerpt_id, - content: content.clone(), - }); + let ix = self + .messages + .iter() + .position(|message| message.excerpt_id == excerpt_id) + .map_or(self.messages.len(), |ix| ix + 1); + self.messages.insert( + ix, + Message { + excerpt_id: new_excerpt_id, + content: content.clone(), + }, + ); self.messages_metadata.insert( - excerpt_id, + new_excerpt_id, MessageMetadata { role, sent_at: Local::now(), error: None, }, ); - (excerpt_id, content) + (new_excerpt_id, content) } fn summarize(&mut self, cx: &mut ModelContext) { @@ -882,7 +899,7 @@ impl AssistantEditor { if metadata.role == Role::User { assistant.assist(cx); } else { - assistant.push_message(Role::User, cx); + assistant.insert_message_after(excerpt_id, Role::User, cx); } } } @@ -1227,3 +1244,28 @@ async fn stream_completion( } } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::AppContext; + + #[gpui::test] + fn test_inserting_and_removing_messages(cx: &mut AppContext) { + let registry = Arc::new(LanguageRegistry::test()); + + cx.add_model(|cx| { + let mut assistant = Assistant::new(Default::default(), registry, cx); + let (excerpt_1, _) = + assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let (excerpt_2, _) = assistant.insert_message_after(excerpt_1, Role::User, cx); + let (excerpt_3, _) = assistant.insert_message_after(excerpt_1, Role::User, cx); + assistant.remove_empty_messages( + HashSet::from_iter([excerpt_2, excerpt_3]), + Default::default(), + cx, + ); + assistant + }); + } +} From 2003d3dbe4d5e11384439322592c2158ac51585b Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Tue, 6 Jun 2023 14:39:56 -0400 Subject: [PATCH 084/101] Update initial_user_settings.json --- assets/settings/initial_user_settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index dc79fd7911caeacf7510a142c9769aeb68f878c4..1599814f96657351461c5866fd8abd26ea8dbc43 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -1,7 +1,7 @@ -// Zed settings +// Folder-specific settings // -// For information on how to configure Zed, see the Zed -// documentation: https://zed.dev/docs/configuring-zed +// For information on how to configure folder-specific settings, see the documentation: +// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings // // To see all of Zed's default settings without changing your // custom settings, run the `open default settings` command From dbbd0558c38fa8ef09e995c5ed75b93951cb2cad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 11:46:46 -0700 Subject: [PATCH 085/101] Eliminate assets crate --- Cargo.lock | 18 +---- Cargo.toml | 2 +- crates/ai/Cargo.toml | 2 +- crates/ai/src/ai.rs | 11 ++- crates/assets/Cargo.toml | 14 ---- crates/assets/build.rs | 29 -------- crates/copilot_button/Cargo.toml | 1 - crates/copilot_button/src/copilot_button.rs | 4 +- crates/settings/Cargo.toml | 2 +- crates/settings/src/keymap_file.rs | 76 ++++++++++----------- crates/settings/src/settings.rs | 38 ++++++----- crates/settings/src/settings_file.rs | 3 +- crates/util/Cargo.toml | 1 + crates/util/src/util.rs | 9 +++ crates/vim/Cargo.toml | 1 - crates/vim/src/test/vim_test_context.rs | 2 +- crates/workspace/Cargo.toml | 1 - crates/zed/Cargo.toml | 1 - crates/zed/build.rs | 30 ++++++++ crates/{assets => zed}/src/assets.rs | 4 ++ crates/zed/src/languages.rs | 6 +- crates/zed/src/languages/json.rs | 4 +- crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 39 ++++------- 24 files changed, 142 insertions(+), 160 deletions(-) delete mode 100644 crates/assets/Cargo.toml delete mode 100644 crates/assets/build.rs rename crates/{assets => zed}/src/assets.rs (84%) diff --git a/Cargo.lock b/Cargo.lock index 7f9a5530132f36be1fe38fd5428611aaf29b2aed..a1ba2a4998f34bea26b14d28e909341fb6efc972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,12 +100,12 @@ name = "ai" version = "0.1.0" dependencies = [ "anyhow", - "assets", "collections", "editor", "futures 0.3.28", "gpui", "isahc", + "rust-embed", "serde", "serde_json", "util", @@ -210,15 +210,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" -[[package]] -name = "assets" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "rust-embed", -] - [[package]] name = "async-broadcast" version = "0.4.1" @@ -1402,7 +1393,6 @@ name = "copilot_button" version = "0.1.0" dependencies = [ "anyhow", - "assets", "context_menu", "copilot", "editor", @@ -6114,7 +6104,6 @@ name = "settings" version = "0.1.0" dependencies = [ "anyhow", - "assets", "collections", "fs", "futures 0.3.28", @@ -6123,6 +6112,7 @@ dependencies = [ "lazy_static", "postage", "pretty_assertions", + "rust-embed", "schemars", "serde", "serde_derive", @@ -7749,6 +7739,7 @@ dependencies = [ "lazy_static", "log", "rand 0.8.5", + "rust-embed", "serde", "serde_json", "smol", @@ -7819,7 +7810,6 @@ name = "vim" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-compat", "async-trait", "collections", @@ -8647,7 +8637,6 @@ name = "workspace" version = "0.1.0" dependencies = [ "anyhow", - "assets", "async-recursion 1.0.4", "bincode", "call", @@ -8747,7 +8736,6 @@ dependencies = [ "activity_indicator", "ai", "anyhow", - "assets", "async-compression", "async-recursion 0.3.2", "async-tar", diff --git a/Cargo.toml b/Cargo.toml index f1362e059df6945d6fa528edbbfa4741a6dc06d9..bc614b6783f531a7dc93a97c6a5590ba198834f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "crates/activity_indicator", "crates/ai", - "crates/assets", "crates/auto_update", "crates/breadcrumbs", "crates/call", @@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } rand = { version = "0.8.5" } regex = { version = "1.5" } +rust-embed = { version = "6.3", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index b367a4d43cac845950dc123e66ed0c7be15da1f2..b6c75537573e58bef64f621ca1b981bec1ce85b1 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -9,12 +9,12 @@ path = "src/ai.rs" doctest = false [dependencies] -assets = { path = "../assets"} collections = { path = "../collections"} editor = { path = "../editor" } gpui = { path = "../gpui" } util = { path = "../util" } +rust-embed.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 2a0110510f8481f5f9a84788ab6d51929b4f2363..15d9bf053e392bcde06948dbc446b63431d6bfc7 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; -use assets::Assets; use collections::HashMap; use editor::Editor; use futures::AsyncBufReadExt; @@ -16,6 +15,14 @@ use std::{io, sync::Arc}; use util::channel::{ReleaseChannel, RELEASE_CHANNEL}; use util::{ResultExt, TryFutureExt}; +use rust_embed::RustEmbed; +use std::str; + +#[derive(RustEmbed)] +#[folder = "../../assets/contexts"] +#[exclude = "*.DS_Store"] +pub struct ContextAssets; + actions!(ai, [Assist]); // Data types for chat completion requests @@ -173,7 +180,7 @@ impl Assistant { let assist_task = cx.spawn(|_, mut cx| { async move { // TODO: We should have a get_string method on assets. This is repateated elsewhere. - let content = Assets::get("contexts/system.zmd").unwrap(); + let content = ContextAssets::get("system.zmd").unwrap(); let mut system_message = std::str::from_utf8(content.data.as_ref()) .unwrap() .to_string(); diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml deleted file mode 100644 index eec60ff84c54264856b0e374066258cc0293e682..0000000000000000000000000000000000000000 --- a/crates/assets/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "assets" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/assets.rs" -doctest = false - -[dependencies] -gpui = { path = "../gpui" } -anyhow.workspace = true -rust-embed = { version = "6.3", features = ["include-exclude"] } diff --git a/crates/assets/build.rs b/crates/assets/build.rs deleted file mode 100644 index 8500b2462240f944f7d64f129a2f560f7c7f4268..0000000000000000000000000000000000000000 --- a/crates/assets/build.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::process::Command; - -fn main() { - let output = Command::new("npm") - .current_dir("../../styles") - .args(["install", "--no-save"]) - .output() - .expect("failed to run npm"); - if !output.status.success() { - panic!( - "failed to install theme dependencies {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let output = Command::new("npm") - .current_dir("../../styles") - .args(["run", "build"]) - .output() - .expect("failed to run npm"); - if !output.status.success() { - panic!( - "build script failed {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - println!("cargo:rerun-if-changed=../../styles/src"); -} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 50fbaa64ee49de6ab1e682856eaec3ed16cfb4bc..c93e1920dc9081b94a7babc43ca42ab3305b5c76 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -9,7 +9,6 @@ path = "src/copilot_button.rs" doctest = false [dependencies] -assets = { path = "../assets" } copilot = { path = "../copilot" } editor = { path = "../editor" } fs = { path = "../fs" } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index e34fddd9b9c5f200bf5bb24120089208bba05a3e..2454074d459b4938cbeebadb5cf7cf73589b5d99 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -315,9 +315,7 @@ async fn configure_disabled_globs( let settings_editor = workspace .update(&mut cx, |_, cx| { create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content(&assets::Assets) - .as_ref() - .into() + settings::initial_user_settings_content().as_ref().into() }) })? .await? diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 1ec0ff4a635551e5877f302375e41d0b8ed0fb62..f0396266fc78ff3ce515ce63350f98aa5bc9d2d6 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -12,7 +12,6 @@ doctest = false test-support = ["gpui/test-support", "fs/test-support"] [dependencies] -assets = { path = "../assets" } collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } @@ -25,6 +24,7 @@ futures.workspace = true json_comments = "0.2" lazy_static.workspace = true postage.workspace = true +rust-embed.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0b638da9242b8dbfdfe504cce5fa478a14c53fb0..e607a254bd70d066be2cae4efcb78d82d222a861 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,6 +1,5 @@ -use crate::settings_store::parse_json_with_comments; +use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{Context, Result}; -use assets::Assets; use collections::BTreeMap; use gpui::{keymap_matcher::Binding, AppContext}; use schemars::{ @@ -10,11 +9,11 @@ use schemars::{ }; use serde::Deserialize; use serde_json::{value::RawValue, Value}; -use util::ResultExt; +use util::{asset_str, ResultExt}; #[derive(Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] -pub struct KeymapFileContent(Vec); +pub struct KeymapFile(Vec); #[derive(Deserialize, Default, Clone, JsonSchema)] pub struct KeymapBlock { @@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction { #[derive(Deserialize)] struct ActionWithData(Box, Box); -impl KeymapFileContent { +impl KeymapFile { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { - let content = Assets::get(asset_path).unwrap().data; - let content_str = std::str::from_utf8(content.as_ref()).unwrap(); - Self::parse(content_str)?.add_to_cx(cx) + let content = asset_str::(asset_path); + Self::parse(content.as_ref())?.add_to_cx(cx) } pub fn parse(content: &str) -> Result { @@ -83,40 +81,40 @@ impl KeymapFileContent { } Ok(()) } -} -pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value { - let mut root_schema = SchemaSettings::draft07() - .with(|settings| settings.option_add_null_type = false) - .into_generator() - .into_root_schema_for::(); + pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value { + let mut root_schema = SchemaSettings::draft07() + .with(|settings| settings.option_add_null_type = false) + .into_generator() + .into_root_schema_for::(); - let action_schema = Schema::Object(SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(vec![ - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - enum_values: Some( - action_names - .iter() - .map(|name| Value::String(name.to_string())) - .collect(), - ), - ..Default::default() - }), - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), - ..Default::default() - }), - ]), + let action_schema = Schema::Object(SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), + enum_values: Some( + action_names + .iter() + .map(|name| Value::String(name.to_string())) + .collect(), + ), + ..Default::default() + }), + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), + ..Default::default() + }), + ]), + ..Default::default() + })), ..Default::default() - })), - ..Default::default() - }); + }); - root_schema - .definitions - .insert("KeymapAction".to_owned(), action_schema); + root_schema + .definitions + .insert("KeymapAction".to_owned(), action_schema); - serde_json::to_value(root_schema).unwrap() + serde_json::to_value(root_schema).unwrap() + } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1c131e5916c5d11746881d729cf93a254d0effd0..8c3587d942d438895f21c707086f804d06b4e3f0 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,31 +2,37 @@ mod keymap_file; mod settings_file; mod settings_store; -use gpui::AssetSource; -pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; +use rust_embed::RustEmbed; +use std::{borrow::Cow, str}; +use util::asset_str; + +pub use keymap_file::KeymapFile; pub use settings_file::*; pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; -use std::{borrow::Cow, str}; -pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json"; -const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; -const INITIAL_LOCAL_SETTINGS_ASSET_PATH: &str = "settings/initial_local_settings.json"; +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "settings/*"] +#[include = "keymaps/*"] +#[exclude = "*.DS_Store"] +pub struct SettingsAssets; pub fn default_settings() -> Cow<'static, str> { - asset_str(&assets::Assets, DEFAULT_SETTINGS_ASSET_PATH) + asset_str::("settings/default.json") +} + +pub fn default_keymap() -> Cow<'static, str> { + asset_str::("keymaps/default.json") } -pub fn initial_user_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> { - asset_str(assets, INITIAL_USER_SETTINGS_ASSET_PATH) +pub fn vim_keymap() -> Cow<'static, str> { + asset_str::("keymaps/vim.json") } -pub fn initial_local_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> { - asset_str(assets, INITIAL_LOCAL_SETTINGS_ASSET_PATH) +pub fn initial_user_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_user_settings.json") } -fn asset_str<'a>(assets: &'a dyn AssetSource, path: &str) -> Cow<'a, str> { - match assets.load(path).unwrap() { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } +pub fn initial_local_settings_content() -> Cow<'static, str> { + asset_str::("settings/initial_local_settings.json") } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index edd4fe0d9dea93cb7444656772311f6dbd439e9b..c7ae296469c3e32b6e3234540c98bb511a4de663 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,6 +1,5 @@ use crate::{settings_store::SettingsStore, Setting}; use anyhow::Result; -use assets::Assets; use fs::Fs; use futures::{channel::mpsc, StreamExt}; use gpui::{executor::Background, AppContext}; @@ -111,7 +110,7 @@ async fn load_settings(fs: &Arc) -> Result { Err(err) => { if let Some(e) = err.downcast_ref::() { if e.kind() == ErrorKind::NotFound { - return Ok(crate::initial_user_settings_content(&Assets).to_string()); + return Ok(crate::initial_user_settings_content().to_string()); } } return Err(err); diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 6216d2e47201d7d980fe3d59de1c29fcd82c095d..8d9594fbeb8783104ac5e165173c7fac50c9e0dc 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -21,6 +21,7 @@ isahc.workspace = true smol.workspace = true url = "2.2" rand.workspace = true +rust-embed.workspace = true tempdir = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 5f738132ae0104553ae24f15736fdaaa5dd3b696..79f3c68514c11eadf62acd8ebf1d805085739eb8 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -7,6 +7,7 @@ pub mod paths; pub mod test; use std::{ + borrow::Cow, cmp::{self, Ordering}, ops::{AddAssign, Range, RangeInclusive}, panic::Location, @@ -284,6 +285,14 @@ impl Iterator for RandomCharIter { } } +/// Get an embedded file as a string. +pub fn asset_str(path: &str) -> Cow<'static, str> { + match A::get(path).unwrap().data { + Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), + Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), + } +} + // copy unstable standard feature option unzip // https://github.com/rust-lang/rust/issues/87800 // Remove when this ship in Rust 1.66 or 1.67 diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index ee3144fd566ba4fae33a4333f159c64b6140595a..57d38213798352bf8fad6970306691182e6fb1fb 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f tokio = { version = "1.15", "optional" = true } serde_json.workspace = true -assets = { path = "../assets" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } editor = { path = "../editor" } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 531fbf0bba110847fa4474fbb8b8bde025865a98..3e66d6bb1c0e78b581ffdb328d24d06f08814d7f 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); - settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap(); + settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); }); // Setup search toolbars and keypress hook diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index b22607e20dec0ac9f285c9a66f5df638c5a66809..8606be4944830f9859863f1510ffe3413631b31a 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -19,7 +19,6 @@ test-support = [ ] [dependencies] -assets = { path = "../assets" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c45991cf11fc7a8ed9e654781d6d144a1d6795d5..8add2e71c1135805517ec15d25e3c4448b5d630c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -17,7 +17,6 @@ path = "src/main.rs" [dependencies] activity_indicator = { path = "../activity_indicator" } -assets = { path = "../assets" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 14bf9999fb725c6dae32bfc46341560b662f3d8b..b83afba7474f80f5082168e42fb01a41b850f69a 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -1,3 +1,5 @@ +use std::process::Command; + fn main() { println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); @@ -21,4 +23,32 @@ fn main() { // Register exported Objective-C selectors, protocols, etc println!("cargo:rustc-link-arg=-Wl,-ObjC"); + + // Install dependencies for theme-generation + let output = Command::new("npm") + .current_dir("../../styles") + .args(["install", "--no-save"]) + .output() + .expect("failed to run npm"); + if !output.status.success() { + panic!( + "failed to install theme dependencies {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + // Regenerate themes + let output = Command::new("npm") + .current_dir("../../styles") + .args(["run", "build"]) + .output() + .expect("failed to run npm"); + if !output.status.success() { + panic!( + "build script failed {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + println!("cargo:rerun-if-changed=../../styles/src"); } diff --git a/crates/assets/src/assets.rs b/crates/zed/src/assets.rs similarity index 84% rename from crates/assets/src/assets.rs rename to crates/zed/src/assets.rs index 7d5748e43b7647bbb5967b44696b620117f7c528..6eb8a44f0fb613a637af1fb6e20d2a80a359bf9f 100644 --- a/crates/assets/src/assets.rs +++ b/crates/zed/src/assets.rs @@ -4,6 +4,10 @@ use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "../../assets"] +#[include = "fonts/**/*"] +#[include = "icons/**/*"] +#[include = "themes/**/*"] +#[include = "*.md"] #[exclude = "*.DS_Store"] pub struct Assets; diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 1f2b359af1523212d97de8dfd43cf4bccaae6dfb..3ae564d13b55c83f8035d23d0a1f5879ae2f1382 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -3,6 +3,7 @@ pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; +use util::asset_str; mod c; mod elixir; @@ -179,10 +180,7 @@ fn load_query(name: &str, filename_prefix: &str) -> Option> { for path in LanguageDir::iter() { if let Some(remainder) = path.strip_prefix(name) { if remainder.starts_with(filename_prefix) { - let contents = match LanguageDir::get(path.as_ref()).unwrap().data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - }; + let contents = asset_str::(path.as_ref()); match &mut result { None => result = Some(contents), Some(r) => r.to_mut().push_str(contents.as_ref()), diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 3947c62a6d96d11ba37444b34930f75bb8fbaec1..e1f3da9e0238f0a91179447e7c15ce2df5239ec1 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -6,7 +6,7 @@ use gpui::AppContext; use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; use node_runtime::NodeRuntime; use serde_json::json; -use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore}; +use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; use staff_mode::StaffMode; use std::{ @@ -143,7 +143,7 @@ impl LspAdapter for JsonLspAdapter { }, { "fileMatch": [schema_file_match(&paths::KEYMAP)], - "schema": keymap_file_json_schema(&action_names), + "schema": KeymapFile::generate_json_schema(&action_names), } ] } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f5031beb2ee5774aa1299dadf7fc2756520534a4..03b7eb6b2491b04e05222bfdf775ba85d3fc45ce 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -2,7 +2,6 @@ #![allow(non_snake_case)] use anyhow::{anyhow, Context, Result}; -use assets::Assets; use backtrace::Backtrace; use cli::{ ipc::{self, IpcSender}, @@ -58,7 +57,8 @@ use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use zed::{ - self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, + assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, + languages, menus, }; fn main() { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8d27a378c5c49d6ebf3d7de3b043e3a908423420..a7bd820350d69cba25e8e044ea1100eb567ddf0e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,7 +1,9 @@ +pub mod assets; pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; + use anyhow::Context; use assets::Assets; use breadcrumbs::Breadcrumbs; @@ -30,12 +32,11 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{ - initial_local_settings_content, KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH, -}; +use settings::{initial_local_settings_content, KeymapFile, SettingsStore}; use std::{borrow::Cow, str, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ + asset_str, channel::ReleaseChannel, paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, ResultExt, @@ -149,7 +150,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { open_bundled_file( workspace, - "licenses.md", + asset_str::("licenses.md"), "Open Source License Attribution", "Markdown", cx, @@ -169,9 +170,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.add_action( move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content(&Assets) - .as_ref() - .into() + settings::initial_user_settings_content().as_ref().into() }) .detach_and_log_err(cx); }, @@ -181,7 +180,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { open_bundled_file( workspace, - "keymaps/default.json", + settings::default_keymap(), "Default Key Bindings", "JSON", cx, @@ -194,7 +193,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx: &mut ViewContext| { open_bundled_file( workspace, - DEFAULT_SETTINGS_ASSET_PATH, + settings::default_settings(), "Default Settings", "JSON", cx, @@ -521,11 +520,11 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { pub fn load_default_keymap(cx: &mut AppContext) { for path in ["keymaps/default.json", "keymaps/vim.json"] { - KeymapFileContent::load_asset(path, cx).unwrap(); + KeymapFile::load_asset(path, cx).unwrap(); } if let Some(asset_path) = settings::get::(cx).asset_path() { - KeymapFileContent::load_asset(asset_path, cx).unwrap(); + KeymapFile::load_asset(asset_path, cx).unwrap(); } } @@ -536,7 +535,7 @@ pub fn handle_keymap_file_changes( cx.spawn(move |mut cx| async move { let mut settings_subscription = None; while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) { + if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) { cx.update(|cx| { cx.clear_bindings(); load_default_keymap(cx); @@ -613,11 +612,7 @@ fn open_local_settings_file( if let Some(buffer) = editor.buffer().read(cx).as_singleton() { if buffer.read(cx).is_empty() { buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, initial_local_settings_content(&Assets))], - None, - cx, - ) + buffer.edit([(0..0, initial_local_settings_content())], None, cx) }); } } @@ -693,7 +688,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext, title: &'static str, language: &'static str, cx: &mut ViewContext, @@ -705,13 +700,9 @@ fn open_bundled_file( .update(&mut cx, |workspace, cx| { workspace.with_local_workspace(cx, |workspace, cx| { let project = workspace.project(); - let buffer = project.update(cx, |project, cx| { - let text = Assets::get(asset_path) - .map(|f| f.data) - .unwrap_or_else(|| Cow::Borrowed(b"File not found")); - let text = str::from_utf8(text.as_ref()).unwrap(); + let buffer = project.update(cx, move |project, cx| { project - .create_buffer(text, language, cx) + .create_buffer(text.as_ref(), language, cx) .expect("creating buffers on a local workspace always succeeds") }); let buffer = cx.add_model(|cx| { From 572c59eec4225c790f98597ad4255d880aa98793 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:50:08 +0200 Subject: [PATCH 086/101] chore: Enable full LTO (#2548) Per conversation with Antonio, I've suggested enabling full LTO; right now we use a crate-local ThinLTO, which does not inline function calls across crates. | Configuration | Current main (788f97e) | Thin LTO | Full LTO | |------------------|------------------------|-----------|-----------| | Size in bytes | 158806721 | 155868753 | 111115553 | | % of `main` size | 100% | 98.14% | 69.96% | | Size in bytes (no debug info) | 129186657 | 127942929 | 108281345 | --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index bc614b6783f531a7dc93a97c6a5590ba198834f6..72a93177a9677e52f7cb0399dbc936fd584f6061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,3 +116,4 @@ split-debuginfo = "unpacked" [profile.release] debug = true +lto = "thin" From c1c91dc2e3133a3de774a3ee01e9047d6dee8320 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 6 Jun 2023 21:50:27 +0200 Subject: [PATCH 087/101] chore: bump MSRV to 1.70 (#2573) Add rust-toolchain.toml Release Notes: - N/A --- .github/workflows/ci.yml | 1 + Dockerfile | 2 +- rust-toolchain.toml | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1c16b2d4d2a540da2ff6bbd00f01323b820f12e..215f7ba165df7144134a2ce349545a9ebe4c5df4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,7 @@ jobs: - name: Install Rust run: | rustup set profile minimal + rustup component add rustfmt rustup update stable - name: Checkout repo diff --git a/Dockerfile b/Dockerfile index d3170696c5fc08c67cbca61a203eced42e4eba0d..2a78d37cbbcadd1bd7afaf612be5767a09abb581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.65-bullseye as builder +FROM rust:1.70-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000000000000000000000000000000000..f400973ca7fe657f39c5614a0bce573e55e7120f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.70" From 559a58d7371c849d038ed452cefb92618e46b871 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 6 Jun 2023 22:21:34 +0200 Subject: [PATCH 088/101] Revert "chore: bump MSRV to 1.70" (#2577) Reverts zed-industries/zed#2573 --- .github/workflows/ci.yml | 1 - Dockerfile | 2 +- rust-toolchain.toml | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 215f7ba165df7144134a2ce349545a9ebe4c5df4..f1c16b2d4d2a540da2ff6bbd00f01323b820f12e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: - name: Install Rust run: | rustup set profile minimal - rustup component add rustfmt rustup update stable - name: Checkout repo diff --git a/Dockerfile b/Dockerfile index 2a78d37cbbcadd1bd7afaf612be5767a09abb581..d3170696c5fc08c67cbca61a203eced42e4eba0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.70-bullseye as builder +FROM rust:1.65-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index f400973ca7fe657f39c5614a0bce573e55e7120f..0000000000000000000000000000000000000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "1.70" From 7bfb51ee76c9def7125352e5e5da181e859c442e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 14:20:21 -0700 Subject: [PATCH 089/101] Live-reload tree-sitter queries in development --- crates/language/src/language.rs | 56 +++++++++++++++++++++++++++------ crates/project/src/project.rs | 27 ++++++++++++++-- crates/zed/src/main.rs | 21 +++++++++++++ 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8b4041b852bc6a2545e35209b156e94ac88050bb..19ae94857405e9e9aaa6d27beb7a7742e825bf86 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -34,7 +34,7 @@ use std::{ fmt::Debug, hash::Hash, mem, - ops::Range, + ops::{Not, Range}, path::{Path, PathBuf}, str, sync::{ @@ -500,6 +500,7 @@ struct AvailableLanguage { grammar: tree_sitter::Language, lsp_adapters: Vec>, get_queries: fn(&str) -> LanguageQueries, + loaded: bool, } pub struct LanguageRegistry { @@ -527,6 +528,7 @@ struct LanguageRegistryState { subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, version: usize, + reload_count: usize, } pub struct PendingLanguageServer { @@ -547,6 +549,7 @@ impl LanguageRegistry { subscription: watch::channel(), theme: Default::default(), version: 0, + reload_count: 0, }), language_server_download_dir: None, lsp_binary_statuses_tx, @@ -566,6 +569,14 @@ impl LanguageRegistry { self.executor = Some(executor); } + /// Clear out all of the loaded languages and reload them from scratch. + /// + /// This is useful in development, when queries have changed. + #[cfg(debug_assertions)] + pub fn reload(&self) { + self.state.write().reload(); + } + pub fn register( &self, path: &'static str, @@ -582,6 +593,7 @@ impl LanguageRegistry { grammar, lsp_adapters, get_queries, + loaded: false, }); } @@ -590,7 +602,7 @@ impl LanguageRegistry { let mut result = state .available_languages .iter() - .map(|l| l.config.name.to_string()) + .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string())) .chain(state.languages.iter().map(|l| l.config.name.to_string())) .collect::>(); result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); @@ -603,6 +615,7 @@ impl LanguageRegistry { state .available_languages .iter() + .filter(|l| !l.loaded) .flat_map(|l| l.lsp_adapters.clone()) .chain( state @@ -639,10 +652,17 @@ impl LanguageRegistry { self.state.read().subscription.1.clone() } + /// The number of times that the registry has been changed, + /// by adding languages or reloading. pub fn version(&self) -> usize { self.state.read().version } + /// The number of times that the registry has been reloaded. + pub fn reload_count(&self) -> usize { + self.state.read().reload_count + } + pub fn set_theme(&self, theme: Arc) { let mut state = self.state.write(); state.theme = Some(theme.clone()); @@ -721,7 +741,7 @@ impl LanguageRegistry { if let Some(language) = state .available_languages .iter() - .find(|l| callback(&l.config)) + .find(|l| !l.loaded && callback(&l.config)) .cloned() { let txs = state @@ -743,9 +763,7 @@ impl LanguageRegistry { let language = Arc::new(language); let mut state = this.state.write(); state.add(language.clone()); - state - .available_languages - .retain(|language| language.id != id); + state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { for tx in txs.drain(..) { let _ = tx.send(Ok(language.clone())); @@ -754,9 +772,7 @@ impl LanguageRegistry { } Err(err) => { let mut state = this.state.write(); - state - .available_languages - .retain(|language| language.id != id); + state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { for tx in txs.drain(..) { let _ = tx.send(Err(anyhow!( @@ -905,6 +921,28 @@ impl LanguageRegistryState { self.version += 1; *self.subscription.0.borrow_mut() = (); } + + #[cfg(debug_assertions)] + fn reload(&mut self) { + self.languages.clear(); + self.version += 1; + self.reload_count += 1; + for language in &mut self.available_languages { + language.loaded = false; + } + *self.subscription.0.borrow_mut() = (); + } + + /// Mark the given language a having been loaded, so that the + /// language registry won't try to load it again. + fn mark_language_loaded(&mut self, id: AvailableLanguageId) { + for language in &mut self.available_languages { + if language.id == id { + language.loaded = true; + break; + } + } + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b0d0d670bae382c0e42e8bd75b7a3b8a59a904ff..39d8ea85129b0bdd282e4fac243929457c9b0e57 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -523,7 +523,7 @@ impl Project { _subscriptions: vec![ cx.observe_global::(Self::on_settings_changed) ], - _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), active_entry: None, languages, @@ -592,7 +592,7 @@ impl Project { active_entry: None, collaborators: Default::default(), join_project_response_message_id: response.message_id, - _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), languages, user_store: user_store.clone(), @@ -2238,13 +2238,34 @@ impl Project { } fn maintain_buffer_languages( - languages: &LanguageRegistry, + languages: Arc, cx: &mut ModelContext, ) -> Task<()> { let mut subscription = languages.subscribe(); + let mut prev_reload_count = languages.reload_count(); cx.spawn_weak(|project, mut cx| async move { while let Some(()) = subscription.next().await { if let Some(project) = project.upgrade(&cx) { + // If the language registry has been reloaded, then remove and + // re-assign the languages on all open buffers. + let reload_count = languages.reload_count(); + if reload_count > prev_reload_count { + prev_reload_count = reload_count; + project.update(&mut cx, |this, cx| { + let buffers = this + .opened_buffers + .values() + .filter_map(|b| b.upgrade(cx)) + .collect::>(); + for buffer in buffers { + if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() { + this.unregister_buffer_from_language_servers(&buffer, &f, cx); + buffer.update(cx, |buffer, cx| buffer.set_language(None, cx)); + } + } + }); + } + project.update(&mut cx, |project, cx| { let mut plain_text_buffers = Vec::new(); let mut buffers_with_unknown_injections = Vec::new(); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 03b7eb6b2491b04e05222bfdf775ba85d3fc45ce..2393d0df3b1ce38bd41c56786ad8d232d5734b6b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -160,6 +160,8 @@ fn main() { ai::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); + cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) + .detach(); languages.set_theme(theme::current(cx).clone()); cx.observe_global::({ @@ -660,11 +662,30 @@ async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { Some(()) } +#[cfg(debug_assertions)] +async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { + let mut events = fs + .watch( + "crates/zed/src/languages".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + languages.reload(); + } + Some(()) +} + #[cfg(not(debug_assertions))] async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { None } +#[cfg(not(debug_assertions))] +async fn watch_languages(_: Arc, _: Arc) -> Option<()> { + None +} + fn connect_to_cli( server_name: &str, ) -> Result<(mpsc::Receiver, IpcSender)> { From 3fc2e0754bb0b95da7e16b949707e998d925e6fe Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 14:27:23 -0700 Subject: [PATCH 090/101] Don't apply the 'literal' color to entire markdown code blocks --- crates/zed/src/languages/markdown/highlights.scm | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/languages/markdown/highlights.scm b/crates/zed/src/languages/markdown/highlights.scm index 83bf7b57a48df2b77648dcd33fbb8fdb675c9e51..971c27686803c45a6200a5fc5497c1171b6d77c6 100644 --- a/crates/zed/src/languages/markdown/highlights.scm +++ b/crates/zed/src/languages/markdown/highlights.scm @@ -14,11 +14,11 @@ (list_marker_parenthesis) ] @punctuation.list_marker -[ - (indented_code_block) - (fenced_code_block) - (code_span) -] @text.literal +(code_span) @text.literal + +(fenced_code_block + (info_string + (language) @text.literal)) (link_destination) @link_uri (link_text) @link_text From dfd72770e76eea1e99f11fdaa32303bf3b65d7ee Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 6 Jun 2023 23:49:34 +0200 Subject: [PATCH 091/101] chore: bump MSRV to 1.70, add rust-toolchain (#2580) This time I've added a `components` section to rust-toolchain.toml file to explicitly require a rustfmt component. Fingers crossed. --- Dockerfile | 2 +- rust-toolchain.toml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 rust-toolchain.toml diff --git a/Dockerfile b/Dockerfile index d3170696c5fc08c67cbca61a203eced42e4eba0d..2a78d37cbbcadd1bd7afaf612be5767a09abb581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.65-bullseye as builder +FROM rust:1.70-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000000000000000000000000000000000..7046039a2c1916415f679ccc3bf5f139895482f6 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.70" +components = [ "rustfmt" ] From cc63d3d048e37da73bdccbdbce3a7583703ced50 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 7 Jun 2023 00:12:47 +0200 Subject: [PATCH 092/101] chore: add targets to rust-toolchain.toml (#2581) --- rust-toolchain.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7046039a2c1916415f679ccc3bf5f139895482f6..f78a67ddb344b48057437e80661698500a1cb302 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] channel = "1.70" components = [ "rustfmt" ] +targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From 6737ee14958dacdaea5d5ec3015ae5798847574d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 16:04:27 -0700 Subject: [PATCH 093/101] Avoid panic when failing to load a language's queries --- crates/language/src/language.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 19ae94857405e9e9aaa6d27beb7a7742e825bf86..32d74c9aefea637572643974b4b0439ba392902d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -771,6 +771,7 @@ impl LanguageRegistry { } } Err(err) => { + log::error!("failed to load language {name} - {err}"); let mut state = this.state.write(); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { @@ -1059,34 +1060,22 @@ impl Language { pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { - self = self - .with_highlights_query(query.as_ref()) - .expect("failed to evaluate highlights query"); + self = self.with_highlights_query(query.as_ref())?; } if let Some(query) = queries.brackets { - self = self - .with_brackets_query(query.as_ref()) - .expect("failed to load brackets query"); + self = self.with_brackets_query(query.as_ref())?; } if let Some(query) = queries.indents { - self = self - .with_indents_query(query.as_ref()) - .expect("failed to load indents query"); + self = self.with_indents_query(query.as_ref())?; } if let Some(query) = queries.outline { - self = self - .with_outline_query(query.as_ref()) - .expect("failed to load outline query"); + self = self.with_outline_query(query.as_ref())?; } if let Some(query) = queries.injections { - self = self - .with_injection_query(query.as_ref()) - .expect("failed to load injection query"); + self = self.with_injection_query(query.as_ref())?; } if let Some(query) = queries.overrides { - self = self - .with_override_query(query.as_ref()) - .expect("failed to load override query"); + self = self.with_override_query(query.as_ref())?; } Ok(self) } From 8dc679e74e8a1d30b2ad1173d3abfbc8a688ddc9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 16:04:46 -0700 Subject: [PATCH 094/101] Upgrade tree-sitter-elixir --- Cargo.lock | 4 ++-- crates/zed/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1ba2a4998f34bea26b14d28e909341fb6efc972..6dbfa27309c32a5b952bc0322db1e2116fc26d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7342,8 +7342,8 @@ dependencies = [ [[package]] name = "tree-sitter-elixir" -version = "0.19.0" -source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" +version = "0.1.0" +source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e" dependencies = [ "cc", "tree-sitter", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8add2e71c1135805517ec15d25e3c4448b5d630c..54919445cb2a3326000d2acb8b9ae99021bd206e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -106,7 +106,7 @@ tree-sitter = "0.20" tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } -tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } +tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } tree-sitter-embedded-template = "0.20.0" tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } From 7aeaa846579a1588747e748e3b65e922e6af4597 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 15:12:07 -0700 Subject: [PATCH 095/101] Fix the order of some patterns in elixir highlight query --- .../zed/src/languages/elixir/highlights.scm | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/zed/src/languages/elixir/highlights.scm index 5c256f341cd5a2e4e045ec4cc3610eabeac3a88a..deea51c436386eb36b1ed41d61cb5d21a787ad20 100644 --- a/crates/zed/src/languages/elixir/highlights.scm +++ b/crates/zed/src/languages/elixir/highlights.scm @@ -1,20 +1,5 @@ ["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword -(unary_operator - operator: "@" @comment.doc - operand: (call - target: (identifier) @comment.doc.__attribute__ - (arguments - [ - (string) @comment.doc - (charlist) @comment.doc - (sigil - quoted_start: _ @comment.doc - quoted_end: _ @comment.doc) @comment.doc - (boolean) @comment.doc - ])) - (#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$")) - (unary_operator operator: "&" operand: (integer) @operator) @@ -84,6 +69,11 @@ quoted_start: _ @string.special quoted_end: _ @string.special) @string.special +( + (identifier) @comment.unused + (#match? @comment.unused "^_") +) + (call target: [ (identifier) @function @@ -99,17 +89,12 @@ (binary_operator left: (identifier) @function operator: "when") + (binary_operator + operator: "|>" + right: (identifier)) ]) (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) -(call - target: (identifier) @keyword - (arguments - (binary_operator - operator: "|>" - right: (identifier))) - (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) - (binary_operator operator: "|>" right: (identifier) @function) @@ -127,10 +112,18 @@ (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") ) -( - (identifier) @comment.unused - (#match? @comment.unused "^_") -) +(unary_operator + operator: "@" @comment.doc + operand: (call + target: (identifier) @__attribute__ @comment.doc + (arguments + [ + (string) + (charlist) + (sigil) + (boolean) + ] @comment.doc)) + (#match? @__attribute__ "^(moduledoc|typedoc|doc)$")) (comment) @comment From 4b9a3c66e6e51716d3b16912ad6a60c9724674e2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 6 Jun 2023 16:06:01 -0700 Subject: [PATCH 096/101] Show function parameters in elixir outline view Introduce a new capture in the outline query called 'context.extra', which causes text to appear in the outline, but not in the breadcrumbs. --- crates/language/src/buffer.rs | 13 ++++-- crates/language/src/buffer_tests.rs | 46 +++++++++++++++++++++ crates/language/src/language.rs | 4 ++ crates/zed/src/languages/elixir/indents.scm | 4 +- crates/zed/src/languages/elixir/outline.scm | 14 ++++++- crates/zed/src/languages/typescript.rs | 6 +-- 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7acb36a92f8f87ed7f0d18b093a5cefe7068f6b1..e09ee48da630989774abdbf0bdffd386afa44a1b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2253,7 +2253,7 @@ impl BufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - self.outline_items_containing(0..self.len(), theme) + self.outline_items_containing(0..self.len(), true, theme) .map(Outline::new) } @@ -2265,6 +2265,7 @@ impl BufferSnapshot { let position = position.to_offset(self); let mut items = self.outline_items_containing( position.saturating_sub(1)..self.len().min(position + 1), + false, theme, )?; let mut prev_depth = None; @@ -2279,6 +2280,7 @@ impl BufferSnapshot { fn outline_items_containing( &self, range: Range, + include_extra_context: bool, theme: Option<&SyntaxTheme>, ) -> Option>> { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { @@ -2313,7 +2315,10 @@ impl BufferSnapshot { let node_is_name; if capture.index == config.name_capture_ix { node_is_name = true; - } else if Some(capture.index) == config.context_capture_ix { + } else if Some(capture.index) == config.context_capture_ix + || (Some(capture.index) == config.extra_context_capture_ix + && include_extra_context) + { node_is_name = false; } else { continue; @@ -2340,10 +2345,12 @@ impl BufferSnapshot { buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end, true, ); + let mut last_buffer_range_end = 0; for (buffer_range, is_name) in buffer_ranges { - if !text.is_empty() { + if !text.is_empty() && buffer_range.start > last_buffer_range_end { text.push(' '); } + last_buffer_range_end = buffer_range.end; if is_name { let mut start = text.len(); let end = start + buffer_range.len(); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index be573aa8956e3dc28e7074d17f85160fe5f2d1e9..9f44de40ac1f4010f7e335277c38e594650f0140 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { + let language = javascript_lang() + .with_outline_query( + r#" + (function_declaration + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context.extra + ")" @context.extra)) @item + "#, + ) + .unwrap(); + + let text = r#" + function a() {} + function b(c) {} + "# + .unindent(); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + + // extra context nodes are included in the outline. + let outline = snapshot.outline(None).unwrap(); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[("function a()", 0), ("function b( )", 0),] + ); + + // extra context nodes do not appear in breadcrumbs. + let symbols = snapshot.symbols_containing(3, None).unwrap(); + assert_eq!( + symbols + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[("function a", 0)] + ); +} + #[gpui::test] async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { let text = r#" diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 32d74c9aefea637572643974b4b0439ba392902d..0ff1d973d3ff47d7e67a82a7639ddcac810702eb 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -455,6 +455,7 @@ struct OutlineConfig { item_capture_ix: u32, name_capture_ix: u32, context_capture_ix: Option, + extra_context_capture_ix: Option, } struct InjectionConfig { @@ -1091,12 +1092,14 @@ impl Language { let mut item_capture_ix = None; let mut name_capture_ix = None; let mut context_capture_ix = None; + let mut extra_context_capture_ix = None; get_capture_indices( &query, &mut [ ("item", &mut item_capture_ix), ("name", &mut name_capture_ix), ("context", &mut context_capture_ix), + ("context.extra", &mut extra_context_capture_ix), ], ); if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { @@ -1105,6 +1108,7 @@ impl Language { item_capture_ix, name_capture_ix, context_capture_ix, + extra_context_capture_ix, }); } Ok(self) diff --git a/crates/zed/src/languages/elixir/indents.scm b/crates/zed/src/languages/elixir/indents.scm index e4139841fc2d2d0ec0bc03f6f76786b76621f77e..ab6fc4da67c95aa1ba8fabdbc10d53b5d7c5e40e 100644 --- a/crates/zed/src/languages/elixir/indents.scm +++ b/crates/zed/src/languages/elixir/indents.scm @@ -1,6 +1,4 @@ -[ - (call) -] @indent +(call) @indent (_ "[" "]" @end) @indent (_ "{" "}" @end) @indent diff --git a/crates/zed/src/languages/elixir/outline.scm b/crates/zed/src/languages/elixir/outline.scm index 985c8ffdca68ab420470a2eaa4f71ab658a9ec30..a3311fb6d4640aa4ff5469c638022c1fde02e912 100644 --- a/crates/zed/src/languages/elixir/outline.scm +++ b/crates/zed/src/languages/elixir/outline.scm @@ -8,9 +8,19 @@ (arguments [ (identifier) @name - (call target: (identifier) @name) + (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) (binary_operator - left: (call target: (identifier) @name) + left: (call + target: (identifier) @name + (arguments + "(" @context.extra + _* @context.extra + ")" @context.extra)) operator: "when") ]) (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index a25d7d02ea550b84a88482be86d3501ecea8032d..7d2d580857780a714496a5f8c2c850de36986d26 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -327,10 +327,10 @@ mod tests { .map(|item| (item.text.as_str(), item.depth)) .collect::>(), &[ - ("function a ( )", 0), - ("async function a2 ( )", 1), + ("function a()", 0), + ("async function a2()", 1), ("let b", 0), - ("function getB ( )", 0), + ("function getB()", 0), ("const d", 0), ] ); From c93b6cc599a65a7a074388e5b7577e2cbaf6eaeb Mon Sep 17 00:00:00 2001 From: Joseph Lyons Date: Wed, 7 Jun 2023 00:23:58 -0400 Subject: [PATCH 097/101] Tweak comment wording --- assets/settings/initial_user_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 1599814f96657351461c5866fd8abd26ea8dbc43..2af677da9a3933db7ced27cf6aea9e4f21a1f630 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -1,6 +1,6 @@ // Folder-specific settings // -// For information on how to configure folder-specific settings, see the documentation: +// For a full list of overridable settings, and general information on folder-specific settings, see the documentation: // https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings // // To see all of Zed's default settings without changing your From a6feaf1300c3f28f762ed17c5e8d98c3d6a001c4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 7 Jun 2023 09:24:18 +0200 Subject: [PATCH 098/101] Allow search assistant editors --- crates/ai/src/assistant.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 7f387194b35634f6364869d9798ce76ef5567b37..aee224e420eef6fc32da9f7990d78e86f57e71cc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -221,6 +221,8 @@ impl AssistantPanel { cx.focus_self(); cx.notify(); } + } else { + cx.propagate_action(); } } @@ -1144,6 +1146,13 @@ impl Item for AssistantEditor { fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { Some(self.title(cx).into()) } + + fn as_searchable( + &self, + _: &ViewHandle, + ) -> Option> { + Some(Box::new(self.editor.clone())) + } } #[derive(Debug)] From 43500dbf6031e5b482e93c4c18ed05dfb0f0fc73 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 7 Jun 2023 10:02:35 +0200 Subject: [PATCH 099/101] Fix zed tests --- crates/zed/src/zed.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f58ea69b93666d6f38ae162247cb6c4404d6bbfc..ecdd1b7a180cee33fa938a39d901022855fe538a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2200,6 +2200,7 @@ mod tests { pane::init(cx); project_panel::init(cx); terminal_view::init(cx); + ai::init(cx); app_state }) } From d26cc2c897732ddb1fe2dac34aabd25e4df6b469 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 7 Jun 2023 15:01:50 +0200 Subject: [PATCH 100/101] Maintain scroll bottom when streaming assistant responses --- crates/ai/src/assistant.rs | 205 +++++++++++++++++++++------- crates/editor/src/editor_tests.rs | 6 +- crates/editor/src/items.rs | 10 +- crates/editor/src/scroll.rs | 22 +-- crates/editor/src/scroll/actions.rs | 6 +- crates/vim/src/normal.rs | 2 +- 6 files changed, 176 insertions(+), 75 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index aee224e420eef6fc32da9f7990d78e86f57e71cc..1a6da539fa4667761367d48d585153c6846f40c7 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -5,13 +5,21 @@ use crate::{ use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; -use editor::{Anchor, Editor, ExcerptId, ExcerptRange, MultiBuffer}; +use editor::{ + display_map::ToDisplayPoint, + scroll::{ + autoscroll::{Autoscroll, AutoscrollStrategy}, + ScrollAnchor, + }, + Anchor, DisplayPoint, Editor, ExcerptId, ExcerptRange, MultiBuffer, +}; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ actions, elements::*, executor::Background, + geometry::vector::vec2f, platform::{CursorStyle, MouseButton}, Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -414,6 +422,7 @@ impl Panel for AssistantPanel { enum AssistantEvent { MessagesEdited { ids: Vec }, SummaryChanged, + StreamedCompletion, } struct Assistant { @@ -531,7 +540,7 @@ impl Assistant { cx.notify(); } - fn assist(&mut self, cx: &mut ModelContext) { + fn assist(&mut self, cx: &mut ModelContext) -> Option<(Message, Message)> { let messages = self .messages .iter() @@ -548,24 +557,30 @@ impl Assistant { stream: true, }; - let api_key = self.api_key.borrow().clone(); - if let Some(api_key) = api_key { - let stream = stream_completion(api_key, cx.background().clone(), request); - let (excerpt_id, content) = - self.insert_message_after(ExcerptId::max(), Role::Assistant, cx); - self.insert_message_after(ExcerptId::max(), Role::User, cx); - let task = cx.spawn_weak(|this, mut cx| async move { + let api_key = self.api_key.borrow().clone()?; + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let user_message = self.insert_message_after(ExcerptId::max(), Role::User, cx); + let task = cx.spawn_weak({ + let assistant_message = assistant_message.clone(); + |this, mut cx| async move { + let assistant_message = assistant_message; let stream_completion = async { let mut messages = stream.await?; while let Some(message) = messages.next().await { let mut message = message?; if let Some(choice) = message.choices.pop() { - content.update(&mut cx, |content, cx| { + assistant_message.content.update(&mut cx, |content, cx| { let text: Arc = choice.delta.content?.into(); content.edit([(content.len()..content.len(), text)], None, cx); Some(()) }); + this.upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))? + .update(&mut cx, |_, cx| { + cx.emit(AssistantEvent::StreamedCompletion); + }); } } @@ -580,23 +595,28 @@ impl Assistant { anyhow::Ok(()) }; - if let Err(error) = stream_completion.await { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = this.messages_metadata.get_mut(&excerpt_id) { + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Err(error) = result { + if let Some(metadata) = this + .messages_metadata + .get_mut(&assistant_message.excerpt_id) + { metadata.error = Some(error.to_string().trim().into()); cx.notify(); } - }); - } + } + }); } - }); + } + }); - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - } + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _task: task, + }); + Some((assistant_message, user_message)) } fn cancel_last_assist(&mut self) -> bool { @@ -646,7 +666,7 @@ impl Assistant { excerpt_id: ExcerptId, role: Role, cx: &mut ModelContext, - ) -> (ExcerptId, ModelHandle) { + ) -> Message { let content = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); let markdown = self.languages.language_for_name("Markdown"); @@ -684,13 +704,11 @@ impl Assistant { .iter() .position(|message| message.excerpt_id == excerpt_id) .map_or(self.messages.len(), |ix| ix + 1); - self.messages.insert( - ix, - Message { - excerpt_id: new_excerpt_id, - content: content.clone(), - }, - ); + let message = Message { + excerpt_id: new_excerpt_id, + content: content.clone(), + }; + self.messages.insert(ix, message.clone()); self.messages_metadata.insert( new_excerpt_id, MessageMetadata { @@ -699,7 +717,7 @@ impl Assistant { error: None, }, ); - (new_excerpt_id, content) + message } fn summarize(&mut self, cx: &mut ModelContext) { @@ -766,6 +784,7 @@ enum AssistantEditorEvent { struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, + scroll_bottom: ScrollAnchor, _subscriptions: Vec, } @@ -875,37 +894,64 @@ impl AssistantEditor { let _subscriptions = vec![ cx.observe(&assistant, |_, _, cx| cx.notify()), cx.subscribe(&assistant, Self::handle_assistant_event), + cx.subscribe(&editor, Self::handle_editor_event), ]; Self { assistant, editor, + scroll_bottom: ScrollAnchor { + offset: Default::default(), + anchor: Anchor::max(), + }, _subscriptions, } } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - self.assistant.update(cx, |assistant, cx| { + let user_message = self.assistant.update(cx, |assistant, cx| { let editor = self.editor.read(cx); let newest_selection = editor.selections.newest_anchor(); let excerpt_id = if newest_selection.head() == Anchor::min() { - assistant.messages.first().map(|message| message.excerpt_id) + assistant + .messages + .first() + .map(|message| message.excerpt_id)? } else if newest_selection.head() == Anchor::max() { - assistant.messages.last().map(|message| message.excerpt_id) + assistant + .messages + .last() + .map(|message| message.excerpt_id)? } else { - Some(newest_selection.head().excerpt_id()) + newest_selection.head().excerpt_id() }; - if let Some(excerpt_id) = excerpt_id { - if let Some(metadata) = assistant.messages_metadata.get(&excerpt_id) { - if metadata.role == Role::User { - assistant.assist(cx); - } else { - assistant.insert_message_after(excerpt_id, Role::User, cx); - } - } - } + let metadata = assistant.messages_metadata.get(&excerpt_id)?; + let user_message = if metadata.role == Role::User { + let (_, user_message) = assistant.assist(cx)?; + user_message + } else { + let user_message = assistant.insert_message_after(excerpt_id, Role::User, cx); + user_message + }; + Some(user_message) }); + + if let Some(user_message) = user_message { + self.editor.update(cx, |editor, cx| { + let cursor = editor + .buffer() + .read(cx) + .snapshot(cx) + .anchor_in_excerpt(user_message.excerpt_id, language::Anchor::MIN); + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + cx, + |selections| selections.select_anchor_ranges([cursor..cursor]), + ); + }); + self.update_scroll_bottom(cx); + } } fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { @@ -919,7 +965,7 @@ impl AssistantEditor { fn handle_assistant_event( &mut self, - assistant: ModelHandle, + _: ModelHandle, event: &AssistantEvent, cx: &mut ViewContext, ) { @@ -931,16 +977,70 @@ impl AssistantEditor { .map(|selection| selection.head()) .collect::>(); let ids = ids.iter().copied().collect::>(); - assistant.update(cx, |assistant, cx| { + self.assistant.update(cx, |assistant, cx| { assistant.remove_empty_messages(ids, selection_heads, cx) }); } AssistantEvent::SummaryChanged => { cx.emit(AssistantEditorEvent::TabContentChanged); } + AssistantEvent::StreamedCompletion => { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let scroll_bottom_row = self + .scroll_bottom + .anchor + .to_display_point(&snapshot.display_snapshot) + .row(); + + let scroll_bottom = scroll_bottom_row as f32 + self.scroll_bottom.offset.y(); + let visible_line_count = editor.visible_line_count().unwrap_or(0.); + let scroll_top = scroll_bottom - visible_line_count; + editor + .set_scroll_position(vec2f(self.scroll_bottom.offset.x(), scroll_top), cx); + }); + } + } + } + + fn handle_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::ScrollPositionChanged { .. } => self.update_scroll_bottom(cx), + _ => {} } } + fn update_scroll_bottom(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let scroll_position = editor + .scroll_manager + .anchor() + .scroll_position(&snapshot.display_snapshot); + let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); + let scroll_bottom_point = cmp::min( + DisplayPoint::new(scroll_bottom.floor() as u32, 0), + snapshot.display_snapshot.max_point(), + ); + let scroll_bottom_anchor = snapshot + .buffer_snapshot + .anchor_after(scroll_bottom_point.to_point(&snapshot.display_snapshot)); + let scroll_bottom_offset = vec2f( + scroll_position.x(), + scroll_bottom - scroll_bottom_point.row() as f32, + ); + self.scroll_bottom = ScrollAnchor { + anchor: scroll_bottom_anchor, + offset: scroll_bottom_offset, + }; + }); + } + fn quote_selection( workspace: &mut Workspace, _: &QuoteSelection, @@ -1155,7 +1255,7 @@ impl Item for AssistantEditor { } } -#[derive(Debug)] +#[derive(Clone, Debug)] struct Message { excerpt_id: ExcerptId, content: ModelHandle, @@ -1265,15 +1365,16 @@ mod tests { cx.add_model(|cx| { let mut assistant = Assistant::new(Default::default(), registry, cx); - let (excerpt_1, _) = - assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); - let (excerpt_2, _) = assistant.insert_message_after(excerpt_1, Role::User, cx); - let (excerpt_3, _) = assistant.insert_message_after(excerpt_1, Role::User, cx); + let message_1 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let message_2 = assistant.insert_message_after(message_1.excerpt_id, Role::User, cx); + let message_3 = assistant.insert_message_after(message_1.excerpt_id, Role::User, cx); assistant.remove_empty_messages( - HashSet::from_iter([excerpt_2, excerpt_3]), + HashSet::from_iter([message_2.excerpt_id, message_3.excerpt_id]), Default::default(), cx, ); + assert_eq!(assistant.messages.len(), 1); + assert_eq!(assistant.messages[0].excerpt_id, message_1.excerpt_id); assistant }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index dca6f71797e068d39ec3419b150a2e93bc03c60c..a63f3404d3fbc79e4fbe31f8b0a8844a710a3d1b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -579,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor; + let mut invalid_anchor = editor.scroll_manager.anchor().anchor; invalid_anchor.text_anchor.buffer_id = Some(999); let invalid_point = Point::new(9999, 0); editor.navigate( @@ -587,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { cursor_anchor: invalid_anchor, cursor_position: invalid_point, scroll_anchor: ScrollAnchor { - top_anchor: invalid_anchor, + anchor: invalid_anchor, offset: Default::default(), }, scroll_top_row: invalid_point.row, @@ -5815,7 +5815,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); follower.set_scroll_anchor( ScrollAnchor { - top_anchor, + anchor: top_anchor, offset: vec2f(0.0, 0.5), }, cx, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8da746075e25f7130b739b82d40e33ceb2ef8130..9d639f9b7bd6aeebe619b9382862c75f0347c7ad 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -196,7 +196,7 @@ impl FollowableItem for Editor { singleton: buffer.is_singleton(), title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), excerpts, - scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)), + scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)), scroll_x: scroll_anchor.offset.x(), scroll_y: scroll_anchor.offset.y(), selections: self @@ -253,7 +253,7 @@ impl FollowableItem for Editor { } Event::ScrollPositionChanged { .. } => { let scroll_anchor = self.scroll_manager.anchor(); - update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor)); + update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); update.scroll_x = scroll_anchor.offset.x(); update.scroll_y = scroll_anchor.offset.y(); true @@ -412,7 +412,7 @@ async fn update_editor_from_message( } else if let Some(scroll_top_anchor) = scroll_top_anchor { editor.set_scroll_anchor_remote( ScrollAnchor { - top_anchor: scroll_top_anchor, + anchor: scroll_top_anchor, offset: vec2f(message.scroll_x, message.scroll_y), }, cx, @@ -510,8 +510,8 @@ impl Item for Editor { }; let mut scroll_anchor = data.scroll_anchor; - if !buffer.can_resolve(&scroll_anchor.top_anchor) { - scroll_anchor.top_anchor = buffer.anchor_before( + if !buffer.can_resolve(&scroll_anchor.anchor) { + scroll_anchor.anchor = buffer.anchor_before( buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), ); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 21894dea88fbf68f9e93fd120082daa60714e2b7..17e8d18a625434b2fd81f8ea6938c72729aed423 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool); #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: Vector2F, - pub top_anchor: Anchor, + pub anchor: Anchor, } impl ScrollAnchor { fn new() -> Self { Self { offset: Vector2F::zero(), - top_anchor: Anchor::min(), + anchor: Anchor::min(), } } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { let mut scroll_position = self.offset; - if self.top_anchor != Anchor::min() { - let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32; + if self.anchor != Anchor::min() { + let scroll_top = self.anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); } else { scroll_position.set_y(0.); @@ -59,7 +59,7 @@ impl ScrollAnchor { } pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { - self.top_anchor.to_point(buffer).row + self.anchor.to_point(buffer).row } } @@ -179,7 +179,7 @@ impl ScrollManager { let (new_anchor, top_row) = if scroll_position.y() <= 0. { ( ScrollAnchor { - top_anchor: Anchor::min(), + anchor: Anchor::min(), offset: scroll_position.max(vec2f(0., 0.)), }, 0, @@ -193,7 +193,7 @@ impl ScrollManager { ( ScrollAnchor { - top_anchor, + anchor: top_anchor, offset: vec2f( scroll_position.x(), scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, @@ -322,7 +322,7 @@ impl Editor { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let top_row = scroll_anchor - .top_anchor + .anchor .to_point(&self.buffer().read(cx).snapshot(cx)) .row; self.scroll_manager @@ -337,7 +337,7 @@ impl Editor { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let top_row = scroll_anchor - .top_anchor + .anchor .to_point(&self.buffer().read(cx).snapshot(cx)) .row; self.scroll_manager @@ -377,7 +377,7 @@ impl Editor { let screen_top = self .scroll_manager .anchor - .top_anchor + .anchor .to_display_point(&snapshot); if screen_top > newest_head { @@ -408,7 +408,7 @@ impl Editor { .anchor_at(Point::new(top_row as u32, 0), Bias::Left); let scroll_anchor = ScrollAnchor { offset: Vector2F::new(x, y), - top_anchor, + anchor: top_anchor, }; self.set_scroll_anchor(scroll_anchor, cx); } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index a79b0f24498c27e43969e9fcddd4cf775733a983..da5e3603e7e2326c690ba3e3a80213874cf325b6 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -86,7 +86,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, @@ -113,7 +113,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, @@ -143,7 +143,7 @@ impl Editor { editor.set_scroll_anchor( ScrollAnchor { - top_anchor: new_anchor, + anchor: new_anchor, offset: Default::default(), }, cx, diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 91be1022a3dd7bb448dbbc447dff7175dfdceeb9..1f90d259d3e73801af58621ee4e80d925647e489 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -400,7 +400,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext Date: Wed, 7 Jun 2023 15:24:08 +0200 Subject: [PATCH 101/101] Fix assistant panel tests --- crates/ai/src/assistant.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 1a6da539fa4667761367d48d585153c6846f40c7..77353e1ee497190277218ef747c48a2b2afe3eb4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1365,16 +1365,18 @@ mod tests { cx.add_model(|cx| { let mut assistant = Assistant::new(Default::default(), registry, cx); - let message_1 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); - let message_2 = assistant.insert_message_after(message_1.excerpt_id, Role::User, cx); - let message_3 = assistant.insert_message_after(message_1.excerpt_id, Role::User, cx); + let message_1 = assistant.messages[0].clone(); + let message_2 = assistant.insert_message_after(ExcerptId::max(), Role::Assistant, cx); + let message_3 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx); + let message_4 = assistant.insert_message_after(message_2.excerpt_id, Role::User, cx); assistant.remove_empty_messages( - HashSet::from_iter([message_2.excerpt_id, message_3.excerpt_id]), + HashSet::from_iter([message_3.excerpt_id, message_4.excerpt_id]), Default::default(), cx, ); - assert_eq!(assistant.messages.len(), 1); + assert_eq!(assistant.messages.len(), 2); assert_eq!(assistant.messages[0].excerpt_id, message_1.excerpt_id); + assert_eq!(assistant.messages[1].excerpt_id, message_2.excerpt_id); assistant }); }