From d77e553466155f2511fbfbeb4ab2de04a956d1a6 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Fri, 29 Mar 2024 13:55:01 -0700 Subject: [PATCH] File context for assistant panel (#9712) Introducing the Active File Context portion of #9705. When someone is in the assistant panel it now includes the active file as a system message on send while showing them a nice little display in the lower right: ![image](https://github.com/zed-industries/zed/assets/836375/9abc56e0-e8f2-45ee-9e7e-b83b28b483ea) For this iteration, I'd love to see the following before we land this: * [x] Toggle-able context - user should be able to disable sending this context * [x] Show nothing if there is no context coming in * [x] Update token count as we change items * [x] Listen for a more finely scoped event for when the active item changes * [x] Create a global for pulling a file icon based on a path. Zed's main way to do this is nested within project panel's `FileAssociation`s. * [x] Get the code fence name for a Language for the system prompt * [x] Update the token count when the buffer content changes I'm seeing this PR as the foundation for providing other kinds of context -- diagnostic summaries, failing tests, additional files, etc. Release Notes: - Added file context to assistant chat panel ([#9705](https://github.com/zed-industries/zed/issues/9705)). image --------- Co-authored-by: Conrad Irwin Co-authored-by: Nathan Co-authored-by: Antonio Scandurra Co-authored-by: Mikayla Maki --- Cargo.lock | 15 ++ Cargo.toml | 2 + crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant.rs | 2 + crates/assistant/src/assistant_panel.rs | 243 +++++++++++++++--- crates/assistant/src/embedded_scope.rs | 91 +++++++ crates/file_icons/Cargo.toml | 21 ++ .../src/file_icons.rs} | 12 +- crates/language/src/language.rs | 10 + crates/languages/src/bash/config.toml | 1 + crates/languages/src/gomod/config.toml | 1 + crates/languages/src/gowork/config.toml | 1 + .../languages/src/ocaml-interface/config.toml | 1 + crates/languages/src/vue/config.toml | 1 + crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 13 +- crates/workspace/src/workspace.rs | 6 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 2 +- .../csharp/languages/csharp/config.toml | 1 + 20 files changed, 377 insertions(+), 49 deletions(-) create mode 100644 crates/assistant/src/embedded_scope.rs create mode 100644 crates/file_icons/Cargo.toml rename crates/{project_panel/src/file_associations.rs => file_icons/src/file_icons.rs} (90%) diff --git a/Cargo.lock b/Cargo.lock index 868f5ee1c1..e466e00b43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,7 @@ dependencies = [ "ctor", "editor", "env_logger", + "file_icons", "fs", "futures 0.3.28", "gpui", @@ -3751,6 +3752,18 @@ dependencies = [ "workspace", ] +[[package]] +name = "file_icons" +version = "0.1.0" +dependencies = [ + "collections", + "gpui", + "serde", + "serde_derive", + "serde_json", + "util", +] + [[package]] name = "filetime" version = "0.2.22" @@ -7235,6 +7248,7 @@ dependencies = [ "collections", "db", "editor", + "file_icons", "gpui", "language", "menu", @@ -12591,6 +12605,7 @@ dependencies = [ "extensions_ui", "feedback", "file_finder", + "file_icons", "fs", "futures 0.3.28", "go_to_line", diff --git a/Cargo.toml b/Cargo.toml index 485daad4b6..07a2253070 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "crates/feature_flags", "crates/feedback", "crates/file_finder", + "crates/file_icons", "crates/fs", "crates/fsevent", "crates/fuzzy", @@ -144,6 +145,7 @@ extensions_ui = { path = "crates/extensions_ui" } feature_flags = { path = "crates/feature_flags" } feedback = { path = "crates/feedback" } file_finder = { path = "crates/file_finder" } +file_icons = { path = "crates/file_icons" } fs = { path = "crates/fs" } fsevent = { path = "crates/fsevent" } fuzzy = { path = "crates/fuzzy" } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index a1e0f98a00..9645977886 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -16,6 +16,7 @@ client.workspace = true collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true +file_icons.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index c5ba22e623..9d72b512a1 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -6,6 +6,8 @@ mod prompts; mod saved_conversation; mod streaming_diff; +mod embedded_scope; + pub use assistant_panel::AssistantPanel; use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel}; use chrono::{DateTime, Local}; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b721521372..06447616c3 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,13 +1,14 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel}, codegen::{self, Codegen, CodegenKind}, + embedded_scope::EmbeddedScope, prompts::generate_content_prompt, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ @@ -16,9 +17,10 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, ToOffset as _, - ToPoint, + Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MultiBufferSnapshot, + ToOffset as _, ToPoint, }; +use file_icons::FileIcons; use fs::Fs; use futures::StreamExt; use gpui::{ @@ -47,7 +49,7 @@ use uuid::Uuid; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, searchable::Direction, - Save, Toast, ToggleZoom, Toolbar, Workspace, + Event as WorkspaceEvent, Save, Toast, ToggleZoom, Toolbar, Workspace, }; pub fn init(cx: &mut AppContext) { @@ -160,6 +162,11 @@ impl AssistantPanel { ]; let model = CompletionProvider::global(cx).default_model(); + cx.observe_global::(|_, cx| { + cx.notify(); + }) + .detach(); + Self { workspace: workspace_handle, active_conversation_editor: None, @@ -709,18 +716,20 @@ impl AssistantPanel { }); } - fn new_conversation(&mut self, cx: &mut ViewContext) -> View { + fn new_conversation(&mut self, cx: &mut ViewContext) -> Option> { + let workspace = self.workspace.upgrade()?; + let editor = cx.new_view(|cx| { ConversationEditor::new( self.model.clone(), self.languages.clone(), self.fs.clone(), - self.workspace.clone(), + workspace, cx, ) }); self.show_conversation(editor.clone(), cx); - editor + Some(editor) } fn show_conversation( @@ -989,11 +998,15 @@ impl AssistantPanel { .await?; this.update(&mut cx, |this, cx| { + let workspace = workspace + .upgrade() + .ok_or_else(|| anyhow!("workspace dropped"))?; let editor = cx.new_view(|cx| { ConversationEditor::for_conversation(conversation, fs, workspace, cx) }); this.show_conversation(editor, cx); - })?; + anyhow::Ok(()) + })??; Ok(()) }) } @@ -1264,9 +1277,10 @@ struct Summary { done: bool, } -struct Conversation { +pub struct Conversation { id: Option, buffer: Model, + embedded_scope: EmbeddedScope, message_anchors: Vec, messages_metadata: HashMap, next_message_id: MessageId, @@ -1288,6 +1302,7 @@ impl Conversation { fn new( model: LanguageModel, language_registry: Arc, + embedded_scope: EmbeddedScope, cx: &mut ModelContext, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); @@ -1321,7 +1336,9 @@ impl Conversation { pending_save: Task::ready(Ok(())), path: None, buffer, + embedded_scope, }; + let message = MessageAnchor { id: MessageId(post_inc(&mut this.next_message_id.0)), start: language::Anchor::MIN, @@ -1422,6 +1439,7 @@ impl Conversation { pending_save: Task::ready(Ok(())), path: Some(path), buffer, + embedded_scope: EmbeddedScope::new(), }; this.count_remaining_tokens(cx); this @@ -1440,7 +1458,7 @@ impl Conversation { } } - fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { let request = self.to_completion_request(cx); self.pending_token_count = cx.spawn(|this, mut cx| { async move { @@ -1603,7 +1621,7 @@ impl Conversation { } fn to_completion_request(&self, cx: &mut ModelContext) -> LanguageModelRequest { - let request = LanguageModelRequest { + let mut request = LanguageModelRequest { model: self.model.clone(), messages: self .messages(cx) @@ -1613,6 +1631,9 @@ impl Conversation { stop: vec![], temperature: 1.0, }; + + let context_message = self.embedded_scope.message(cx); + request.messages.extend(context_message); request } @@ -2002,17 +2023,18 @@ impl ConversationEditor { model: LanguageModel, language_registry: Arc, fs: Arc, - workspace: WeakView, + workspace: View, cx: &mut ViewContext, ) -> Self { - let conversation = cx.new_model(|cx| Conversation::new(model, language_registry, cx)); + let conversation = cx + .new_model(|cx| Conversation::new(model, language_registry, EmbeddedScope::new(), cx)); Self::for_conversation(conversation, fs, workspace, cx) } fn for_conversation( conversation: Model, fs: Arc, - workspace: WeakView, + workspace: View, cx: &mut ViewContext, ) -> Self { let editor = cx.new_view(|cx| { @@ -2027,6 +2049,7 @@ impl ConversationEditor { cx.observe(&conversation, |_, _, cx| cx.notify()), cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), + cx.subscribe(&workspace, Self::handle_workspace_event), ]; let mut this = Self { @@ -2035,9 +2058,10 @@ impl ConversationEditor { blocks: Default::default(), scroll_position: None, fs, - workspace, + workspace: workspace.downgrade(), _subscriptions, }; + this.update_active_buffer(workspace, cx); this.update_message_headers(cx); this } @@ -2171,6 +2195,37 @@ impl ConversationEditor { } } + fn handle_workspace_event( + &mut self, + workspace: View, + event: &WorkspaceEvent, + cx: &mut ViewContext, + ) { + if let WorkspaceEvent::ActiveItemChanged = event { + self.update_active_buffer(workspace, cx); + } + } + + fn update_active_buffer( + &mut self, + workspace: View, + cx: &mut ViewContext<'_, ConversationEditor>, + ) { + let active_buffer = workspace + .read(cx) + .active_item(cx) + .and_then(|item| Some(item.act_as::(cx)?.read(cx).buffer().clone())); + + self.conversation.update(cx, |conversation, cx| { + conversation + .embedded_scope + .set_active_buffer(active_buffer.clone(), cx); + + conversation.count_remaining_tokens(cx); + cx.notify(); + }); + } + fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); @@ -2304,11 +2359,11 @@ impl ConversationEditor { 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()) + start_language.map(|language| language.code_fence_block_name()) } else { None }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + let language_name = language_name.as_deref().unwrap_or(""); let selected_text = buffer.text_for_range(range).collect::(); let text = if selected_text.is_empty() { @@ -2332,15 +2387,17 @@ impl ConversationEditor { if let Some(text) = text { panel.update(cx, |panel, cx| { - let conversation = panel + if let Some(conversation) = panel .active_conversation_editor() .cloned() - .unwrap_or_else(|| panel.new_conversation(cx)); - conversation.update(cx, |conversation, cx| { - conversation - .editor - .update(cx, |editor, cx| editor.insert(&text, cx)) - }); + .or_else(|| panel.new_conversation(cx)) + { + conversation.update(cx, |conversation, cx| { + conversation + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + }; }); } } @@ -2405,12 +2462,120 @@ impl ConversationEditor { .map(|summary| summary.text.clone()) .unwrap_or_else(|| "New Conversation".into()) } + + fn render_embedded_scope(&self, cx: &mut ViewContext) -> Option { + let active_buffer = self + .conversation + .read(cx) + .embedded_scope + .active_buffer()? + .clone(); + + Some( + div() + .p_4() + .v_flex() + .child( + div() + .h_flex() + .items_center() + .child(Icon::new(IconName::File)) + .child( + div() + .h_6() + .child(Label::new("File Contexts")) + .ml_1() + .font_weight(FontWeight::SEMIBOLD), + ), + ) + .child( + div() + .ml_4() + .child(self.render_active_buffer(active_buffer, cx)), + ), + ) + } + + fn render_active_buffer( + &self, + buffer: Model, + cx: &mut ViewContext, + ) -> impl Element { + let buffer = buffer.read(cx); + let icon_path; + let path; + if let Some(singleton) = buffer.as_singleton() { + let singleton = singleton.read(cx); + + path = singleton.file().map(|file| file.full_path(cx)); + + icon_path = path + .as_ref() + .and_then(|path| FileIcons::get_icon(path.as_path(), cx)) + .map(SharedString::from) + .unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg")); + } else { + icon_path = SharedString::from("icons/file_icons/file.svg"); + path = None; + } + + let file_name = path.map_or("Untitled".to_string(), |path| { + path.to_string_lossy().to_string() + }); + + let enabled = self + .conversation + .read(cx) + .embedded_scope + .active_buffer_enabled(); + + let file_name_text_color = if enabled { + Color::Default + } else { + Color::Disabled + }; + + div() + .id("active-buffer") + .h_flex() + .cursor_pointer() + .child(Icon::from_path(icon_path).color(file_name_text_color)) + .child( + div() + .h_6() + .child(Label::new(file_name).color(file_name_text_color)) + .ml_1(), + ) + .children(enabled.then(|| { + div() + .child(Icon::new(IconName::Check).color(file_name_text_color)) + .ml_1() + })) + .on_click(cx.listener(move |this, _, cx| { + this.conversation.update(cx, |conversation, cx| { + conversation + .embedded_scope + .set_active_buffer_enabled(!enabled); + cx.notify(); + }) + })) + } } impl EventEmitter for ConversationEditor {} impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> impl Element { + // + // The ConversationEditor has two main segments + // + // 1. Messages Editor + // 2. Context + // - File Context (currently only the active file) + // - Project Diagnostics (Planned) + // - Deep Code Context (Planned, for query and other tools for the model) + // + div() .key_context("ConversationEditor") .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) @@ -2420,14 +2585,15 @@ impl Render for ConversationEditor { .on_action(cx.listener(ConversationEditor::assist)) .on_action(cx.listener(ConversationEditor::split)) .size_full() - .relative() + .v_flex() .child( div() - .size_full() + .flex_grow() .pl_4() .bg(cx.theme().colors().editor_background) .child(self.editor.clone()), ) + .child(div().flex_shrink().children(self.render_embedded_scope(cx))) } } @@ -2799,8 +2965,9 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let conversation = - cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); + let conversation = cx.new_model(|cx| { + Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx) + }); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -2931,8 +3098,9 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let conversation = - cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); + let conversation = cx.new_model(|cx| { + Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx) + }); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3030,8 +3198,9 @@ mod tests { cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let conversation = - cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); + let conversation = cx.new_model(|cx| { + Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx) + }); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3115,8 +3284,14 @@ mod tests { cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default())); cx.update(init); let registry = Arc::new(LanguageRegistry::test(cx.executor())); - let conversation = - cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry.clone(), cx)); + let conversation = cx.new_model(|cx| { + Conversation::new( + LanguageModel::default(), + registry.clone(), + EmbeddedScope::new(), + cx, + ) + }); let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone()); let message_0 = conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id); diff --git a/crates/assistant/src/embedded_scope.rs b/crates/assistant/src/embedded_scope.rs new file mode 100644 index 0000000000..2bff3c3bfa --- /dev/null +++ b/crates/assistant/src/embedded_scope.rs @@ -0,0 +1,91 @@ +use editor::MultiBuffer; +use gpui::{AppContext, Model, ModelContext, Subscription}; + +use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role}; + +#[derive(Default)] +pub struct EmbeddedScope { + active_buffer: Option>, + active_buffer_enabled: bool, + active_buffer_subscription: Option, +} + +impl EmbeddedScope { + pub fn new() -> Self { + Self { + active_buffer: None, + active_buffer_enabled: true, + active_buffer_subscription: None, + } + } + + pub fn set_active_buffer( + &mut self, + buffer: Option>, + cx: &mut ModelContext, + ) { + self.active_buffer_subscription.take(); + + if let Some(active_buffer) = buffer.clone() { + self.active_buffer_subscription = + Some(cx.subscribe(&active_buffer, |conversation, _, e, cx| { + if let multi_buffer::Event::Edited { .. } = e { + conversation.count_remaining_tokens(cx) + } + })); + } + + self.active_buffer = buffer; + } + + pub fn active_buffer(&self) -> Option<&Model> { + self.active_buffer.as_ref() + } + + pub fn active_buffer_enabled(&self) -> bool { + self.active_buffer_enabled + } + + pub fn set_active_buffer_enabled(&mut self, enabled: bool) { + self.active_buffer_enabled = enabled; + } + + /// Provide a message for the language model based on the active buffer. + pub fn message(&self, cx: &AppContext) -> Option { + if !self.active_buffer_enabled { + return None; + } + + let active_buffer = self.active_buffer.as_ref()?; + let buffer = active_buffer.read(cx); + + if let Some(singleton) = buffer.as_singleton() { + let singleton = singleton.read(cx); + + let filename = singleton + .file() + .map(|file| file.path().to_string_lossy()) + .unwrap_or("Untitled".into()); + + let text = singleton.text(); + + let language = singleton + .language() + .map(|l| { + let name = l.code_fence_block_name(); + name.to_string() + }) + .unwrap_or_default(); + + let markdown = + format!("User's active file `{filename}`:\n\n```{language}\n{text}```\n\n"); + + return Some(LanguageModelRequestMessage { + role: Role::System, + content: markdown, + }); + } + + None + } +} diff --git a/crates/file_icons/Cargo.toml b/crates/file_icons/Cargo.toml new file mode 100644 index 0000000000..c3e9268997 --- /dev/null +++ b/crates/file_icons/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "file_icons" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/file_icons.rs" +doctest = false + +[dependencies] +gpui.workspace = true +util.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +collections.workspace = true diff --git a/crates/project_panel/src/file_associations.rs b/crates/file_icons/src/file_icons.rs similarity index 90% rename from crates/project_panel/src/file_associations.rs rename to crates/file_icons/src/file_icons.rs index 5db7606ed2..029d0b9979 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/file_icons/src/file_icons.rs @@ -12,13 +12,13 @@ struct TypeConfig { } #[derive(Deserialize, Debug)] -pub struct FileAssociations { +pub struct FileIcons { stems: HashMap, suffixes: HashMap, types: HashMap, } -impl Global for FileAssociations {} +impl Global for FileIcons {} const COLLAPSED_DIRECTORY_TYPE: &str = "collapsed_folder"; const EXPANDED_DIRECTORY_TYPE: &str = "expanded_folder"; @@ -27,18 +27,18 @@ const EXPANDED_CHEVRON_TYPE: &str = "expanded_chevron"; pub const FILE_TYPES_ASSET: &str = "icons/file_icons/file_types.json"; pub fn init(assets: impl AssetSource, cx: &mut AppContext) { - cx.set_global(FileAssociations::new(assets)) + cx.set_global(FileIcons::new(assets)) } -impl FileAssociations { +impl FileIcons { pub fn new(assets: impl AssetSource) -> Self { assets .load("icons/file_icons/file_types.json") .and_then(|file| { - serde_json::from_str::(str::from_utf8(&file).unwrap()) + serde_json::from_str::(str::from_utf8(&file).unwrap()) .map_err(Into::into) }) - .unwrap_or_else(|_| FileAssociations { + .unwrap_or_else(|_| FileIcons { stems: HashMap::default(), suffixes: HashMap::default(), types: HashMap::default(), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fee6b2ac2a..797589c5f6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -486,6 +486,8 @@ pub struct CodeLabel { pub struct LanguageConfig { /// Human-readable name of the language. pub name: Arc, + /// The name of this language for a Markdown code fence block + pub code_fence_block_name: Option>, // The name of the grammar in a WASM bundle (experimental). pub grammar: Option>, /// The criteria for matching this language to a given file. @@ -609,6 +611,7 @@ impl Default for LanguageConfig { fn default() -> Self { Self { name: "".into(), + code_fence_block_name: None, grammar: None, matcher: LanguageMatcher::default(), brackets: Default::default(), @@ -1185,6 +1188,13 @@ impl Language { self.config.name.clone() } + pub fn code_fence_block_name(&self) -> Arc { + self.config + .code_fence_block_name + .clone() + .unwrap_or_else(|| self.config.name.to_lowercase().into()) + } + pub fn context_provider(&self) -> Option> { self.context_provider.clone() } diff --git a/crates/languages/src/bash/config.toml b/crates/languages/src/bash/config.toml index abbb95bda5..47c8f9e28f 100644 --- a/crates/languages/src/bash/config.toml +++ b/crates/languages/src/bash/config.toml @@ -1,4 +1,5 @@ name = "Shell Script" +code_fence_block_name = "bash" grammar = "bash" path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"] line_comments = ["# "] diff --git a/crates/languages/src/gomod/config.toml b/crates/languages/src/gomod/config.toml index b4755fae78..e70c9358bf 100644 --- a/crates/languages/src/gomod/config.toml +++ b/crates/languages/src/gomod/config.toml @@ -1,4 +1,5 @@ name = "Go Mod" +code_fence_block_name = "go.mod" grammar = "gomod" path_suffixes = ["mod"] line_comments = ["//"] diff --git a/crates/languages/src/gowork/config.toml b/crates/languages/src/gowork/config.toml index 5a63dee2ee..68beb073ab 100644 --- a/crates/languages/src/gowork/config.toml +++ b/crates/languages/src/gowork/config.toml @@ -1,4 +1,5 @@ name = "Go Work" +code_fence_block_name = "gowork" grammar = "gowork" path_suffixes = ["work"] line_comments = ["//"] diff --git a/crates/languages/src/ocaml-interface/config.toml b/crates/languages/src/ocaml-interface/config.toml index 4df8074953..9ce77acb6b 100644 --- a/crates/languages/src/ocaml-interface/config.toml +++ b/crates/languages/src/ocaml-interface/config.toml @@ -1,4 +1,5 @@ name = "OCaml Interface" +code_fence_block_name = "ocaml" grammar = "ocaml_interface" path_suffixes = ["mli"] block_comment = ["(* ", "*)"] diff --git a/crates/languages/src/vue/config.toml b/crates/languages/src/vue/config.toml index 5b9d95e408..47e1e3784a 100644 --- a/crates/languages/src/vue/config.toml +++ b/crates/languages/src/vue/config.toml @@ -1,4 +1,5 @@ name = "Vue.js" +code_fence_block_name = "vue" grammar = "vue" path_suffixes = ["vue"] block_comment = [""] diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 2772fc18a2..b7b8f97313 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true collections.workspace = true db.workspace = true editor.workspace = true +file_icons.workspace = true gpui.workspace = true menu.workspace = true pretty_assertions.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c86fcaa7b2..6fb3bcda82 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,11 +1,10 @@ -pub mod file_associations; mod project_panel_settings; use client::{ErrorCode, ErrorExt}; use settings::Settings; use db::kvp::KEY_VALUE_STORE; use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor}; -use file_associations::FileAssociations; +use file_icons::FileIcons; use anyhow::{anyhow, Result}; use collections::{hash_map, HashMap}; @@ -142,7 +141,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); - file_associations::init(assets, cx); + file_icons::init(assets, cx); cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, _: &ToggleFocus, cx| { @@ -229,7 +228,7 @@ impl ProjectPanel { }) .detach(); - cx.observe_global::(|_, cx| { + cx.observe_global::(|_, cx| { cx.notify(); }) .detach(); @@ -1329,16 +1328,16 @@ impl ProjectPanel { let icon = match entry.kind { EntryKind::File(_) => { if show_file_icons { - FileAssociations::get_icon(&entry.path, cx) + FileIcons::get_icon(&entry.path, cx) } else { None } } _ => { if show_folder_icons { - FileAssociations::get_folder_icon(is_expanded, cx) + FileIcons::get_folder_icon(is_expanded, cx) } else { - FileAssociations::get_chevron_icon(is_expanded, cx) + FileIcons::get_chevron_icon(is_expanded, cx) } } }; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bfaa0c74ba..53c5a620b5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -517,6 +517,7 @@ impl DelayedDebouncedEditAction { pub enum Event { PaneAdded(View), + ActiveItemChanged, ContactRequestedJoin(u64), WorkspaceCreated(WeakView), SpawnTask(SpawnInTerminal), @@ -2377,6 +2378,7 @@ impl Workspace { self.update_window_edited(cx); } pane::Event::RemoveItem { item_id } => { + cx.emit(Event::ActiveItemChanged); self.update_window_edited(cx); if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { if entry.get().entity_id() == pane.entity_id() { @@ -2747,10 +2749,12 @@ impl Workspace { .any(|state| state.leader_id == peer_id) } - fn active_item_path_changed(&mut self, cx: &mut WindowContext) { + fn active_item_path_changed(&mut self, cx: &mut ViewContext) { + cx.emit(Event::ActiveItemChanged); let active_entry = self.active_project_path(cx); self.project .update(cx, |project, cx| project.set_active_path(active_entry, cx)); + self.update_window_title(cx); } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2475c2ebfd..599c9ddd16 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -42,6 +42,7 @@ env_logger.workspace = true extension.workspace = true extensions_ui.workspace = true feedback.workspace = true +file_icons.workspace = true file_finder.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 31649d5266..dac37ba7f0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1082,7 +1082,7 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { while (events.next().await).is_some() { cx.update(|cx| { cx.update_global(|file_types, _| { - *file_types = project_panel::file_associations::FileAssociations::new(Assets); + *file_types = file_icons::FileIcons::new(Assets); }); }) .ok(); diff --git a/extensions/csharp/languages/csharp/config.toml b/extensions/csharp/languages/csharp/config.toml index 51a10c70e0..fd0e13b6c2 100644 --- a/extensions/csharp/languages/csharp/config.toml +++ b/extensions/csharp/languages/csharp/config.toml @@ -1,4 +1,5 @@ name = "CSharp" +code_fence_block_name = "csharp" grammar = "c_sharp" path_suffixes = ["cs"] line_comments = ["// "]