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)).

<img width="1558" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/86eb7e50-3e28-4754-9c3f-895be588616d">

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
Kyle Kelley 2024-03-29 13:55:01 -07:00 committed by GitHub
parent df3050dac1
commit d77e553466
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 377 additions and 49 deletions

15
Cargo.lock generated
View File

@ -328,6 +328,7 @@ dependencies = [
"ctor", "ctor",
"editor", "editor",
"env_logger", "env_logger",
"file_icons",
"fs", "fs",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
@ -3751,6 +3752,18 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "file_icons"
version = "0.1.0"
dependencies = [
"collections",
"gpui",
"serde",
"serde_derive",
"serde_json",
"util",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.22" version = "0.2.22"
@ -7235,6 +7248,7 @@ dependencies = [
"collections", "collections",
"db", "db",
"editor", "editor",
"file_icons",
"gpui", "gpui",
"language", "language",
"menu", "menu",
@ -12591,6 +12605,7 @@ dependencies = [
"extensions_ui", "extensions_ui",
"feedback", "feedback",
"file_finder", "file_finder",
"file_icons",
"fs", "fs",
"futures 0.3.28", "futures 0.3.28",
"go_to_line", "go_to_line",

View File

@ -28,6 +28,7 @@ members = [
"crates/feature_flags", "crates/feature_flags",
"crates/feedback", "crates/feedback",
"crates/file_finder", "crates/file_finder",
"crates/file_icons",
"crates/fs", "crates/fs",
"crates/fsevent", "crates/fsevent",
"crates/fuzzy", "crates/fuzzy",
@ -144,6 +145,7 @@ extensions_ui = { path = "crates/extensions_ui" }
feature_flags = { path = "crates/feature_flags" } feature_flags = { path = "crates/feature_flags" }
feedback = { path = "crates/feedback" } feedback = { path = "crates/feedback" }
file_finder = { path = "crates/file_finder" } file_finder = { path = "crates/file_finder" }
file_icons = { path = "crates/file_icons" }
fs = { path = "crates/fs" } fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" } fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" } fuzzy = { path = "crates/fuzzy" }

View File

@ -16,6 +16,7 @@ client.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
editor.workspace = true editor.workspace = true
file_icons.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true

View File

@ -6,6 +6,8 @@ mod prompts;
mod saved_conversation; mod saved_conversation;
mod streaming_diff; mod streaming_diff;
mod embedded_scope;
pub use assistant_panel::AssistantPanel; pub use assistant_panel::AssistantPanel;
use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel}; use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};

View File

@ -1,13 +1,14 @@
use crate::{ use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel}, assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind}, codegen::{self, Codegen, CodegenKind},
embedded_scope::EmbeddedScope,
prompts::generate_content_prompt, prompts::generate_content_prompt,
Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel,
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, SavedMessage, Split, ToggleFocus, ToggleIncludeConversation,
}; };
use anyhow::Result; use anyhow::{anyhow, Result};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use collections::{hash_map, HashMap, HashSet, VecDeque}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
@ -16,9 +17,10 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
}, },
scroll::{Autoscroll, AutoscrollStrategy}, scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, ToOffset as _, Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MultiBufferSnapshot,
ToPoint, ToOffset as _, ToPoint,
}; };
use file_icons::FileIcons;
use fs::Fs; use fs::Fs;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
@ -47,7 +49,7 @@ use uuid::Uuid;
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
searchable::Direction, searchable::Direction,
Save, Toast, ToggleZoom, Toolbar, Workspace, Event as WorkspaceEvent, Save, Toast, ToggleZoom, Toolbar, Workspace,
}; };
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
@ -160,6 +162,11 @@ impl AssistantPanel {
]; ];
let model = CompletionProvider::global(cx).default_model(); let model = CompletionProvider::global(cx).default_model();
cx.observe_global::<FileIcons>(|_, cx| {
cx.notify();
})
.detach();
Self { Self {
workspace: workspace_handle, workspace: workspace_handle,
active_conversation_editor: None, active_conversation_editor: None,
@ -709,18 +716,20 @@ impl AssistantPanel {
}); });
} }
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> { fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ConversationEditor>> {
let workspace = self.workspace.upgrade()?;
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
ConversationEditor::new( ConversationEditor::new(
self.model.clone(), self.model.clone(),
self.languages.clone(), self.languages.clone(),
self.fs.clone(), self.fs.clone(),
self.workspace.clone(), workspace,
cx, cx,
) )
}); });
self.show_conversation(editor.clone(), cx); self.show_conversation(editor.clone(), cx);
editor Some(editor)
} }
fn show_conversation( fn show_conversation(
@ -989,11 +998,15 @@ impl AssistantPanel {
.await?; .await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let workspace = workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?;
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
ConversationEditor::for_conversation(conversation, fs, workspace, cx) ConversationEditor::for_conversation(conversation, fs, workspace, cx)
}); });
this.show_conversation(editor, cx); this.show_conversation(editor, cx);
})?; anyhow::Ok(())
})??;
Ok(()) Ok(())
}) })
} }
@ -1264,9 +1277,10 @@ struct Summary {
done: bool, done: bool,
} }
struct Conversation { pub struct Conversation {
id: Option<String>, id: Option<String>,
buffer: Model<Buffer>, buffer: Model<Buffer>,
embedded_scope: EmbeddedScope,
message_anchors: Vec<MessageAnchor>, message_anchors: Vec<MessageAnchor>,
messages_metadata: HashMap<MessageId, MessageMetadata>, messages_metadata: HashMap<MessageId, MessageMetadata>,
next_message_id: MessageId, next_message_id: MessageId,
@ -1288,6 +1302,7 @@ impl Conversation {
fn new( fn new(
model: LanguageModel, model: LanguageModel,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
embedded_scope: EmbeddedScope,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let markdown = language_registry.language_for_name("Markdown"); let markdown = language_registry.language_for_name("Markdown");
@ -1321,7 +1336,9 @@ impl Conversation {
pending_save: Task::ready(Ok(())), pending_save: Task::ready(Ok(())),
path: None, path: None,
buffer, buffer,
embedded_scope,
}; };
let message = MessageAnchor { let message = MessageAnchor {
id: MessageId(post_inc(&mut this.next_message_id.0)), id: MessageId(post_inc(&mut this.next_message_id.0)),
start: language::Anchor::MIN, start: language::Anchor::MIN,
@ -1422,6 +1439,7 @@ impl Conversation {
pending_save: Task::ready(Ok(())), pending_save: Task::ready(Ok(())),
path: Some(path), path: Some(path),
buffer, buffer,
embedded_scope: EmbeddedScope::new(),
}; };
this.count_remaining_tokens(cx); this.count_remaining_tokens(cx);
this this
@ -1440,7 +1458,7 @@ impl Conversation {
} }
} }
fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) { pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
let request = self.to_completion_request(cx); let request = self.to_completion_request(cx);
self.pending_token_count = cx.spawn(|this, mut cx| { self.pending_token_count = cx.spawn(|this, mut cx| {
async move { async move {
@ -1603,7 +1621,7 @@ impl Conversation {
} }
fn to_completion_request(&self, cx: &mut ModelContext<Conversation>) -> LanguageModelRequest { fn to_completion_request(&self, cx: &mut ModelContext<Conversation>) -> LanguageModelRequest {
let request = LanguageModelRequest { let mut request = LanguageModelRequest {
model: self.model.clone(), model: self.model.clone(),
messages: self messages: self
.messages(cx) .messages(cx)
@ -1613,6 +1631,9 @@ impl Conversation {
stop: vec![], stop: vec![],
temperature: 1.0, temperature: 1.0,
}; };
let context_message = self.embedded_scope.message(cx);
request.messages.extend(context_message);
request request
} }
@ -2002,17 +2023,18 @@ impl ConversationEditor {
model: LanguageModel, model: LanguageModel,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>, workspace: View<Workspace>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> 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) Self::for_conversation(conversation, fs, workspace, cx)
} }
fn for_conversation( fn for_conversation(
conversation: Model<Conversation>, conversation: Model<Conversation>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>, workspace: View<Workspace>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let editor = cx.new_view(|cx| { let editor = cx.new_view(|cx| {
@ -2027,6 +2049,7 @@ impl ConversationEditor {
cx.observe(&conversation, |_, _, cx| cx.notify()), cx.observe(&conversation, |_, _, cx| cx.notify()),
cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&conversation, Self::handle_conversation_event),
cx.subscribe(&editor, Self::handle_editor_event), cx.subscribe(&editor, Self::handle_editor_event),
cx.subscribe(&workspace, Self::handle_workspace_event),
]; ];
let mut this = Self { let mut this = Self {
@ -2035,9 +2058,10 @@ impl ConversationEditor {
blocks: Default::default(), blocks: Default::default(),
scroll_position: None, scroll_position: None,
fs, fs,
workspace, workspace: workspace.downgrade(),
_subscriptions, _subscriptions,
}; };
this.update_active_buffer(workspace, cx);
this.update_message_headers(cx); this.update_message_headers(cx);
this this
} }
@ -2171,6 +2195,37 @@ impl ConversationEditor {
} }
} }
fn handle_workspace_event(
&mut self,
workspace: View<Workspace>,
event: &WorkspaceEvent,
cx: &mut ViewContext<Self>,
) {
if let WorkspaceEvent::ActiveItemChanged = event {
self.update_active_buffer(workspace, cx);
}
}
fn update_active_buffer(
&mut self,
workspace: View<Workspace>,
cx: &mut ViewContext<'_, ConversationEditor>,
) {
let active_buffer = workspace
.read(cx)
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(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<Self>) -> Option<ScrollPosition> { fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
@ -2304,11 +2359,11 @@ impl ConversationEditor {
let start_language = buffer.language_at(range.start); let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end); let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language { let language_name = if start_language == end_language {
start_language.map(|language| language.name()) start_language.map(|language| language.code_fence_block_name())
} else { } else {
None 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::<String>(); let selected_text = buffer.text_for_range(range).collect::<String>();
let text = if selected_text.is_empty() { let text = if selected_text.is_empty() {
@ -2332,15 +2387,17 @@ impl ConversationEditor {
if let Some(text) = text { if let Some(text) = text {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
let conversation = panel if let Some(conversation) = panel
.active_conversation_editor() .active_conversation_editor()
.cloned() .cloned()
.unwrap_or_else(|| panel.new_conversation(cx)); .or_else(|| panel.new_conversation(cx))
conversation.update(cx, |conversation, cx| { {
conversation conversation.update(cx, |conversation, cx| {
.editor conversation
.update(cx, |editor, cx| editor.insert(&text, cx)) .editor
}); .update(cx, |editor, cx| editor.insert(&text, cx))
});
};
}); });
} }
} }
@ -2405,12 +2462,120 @@ impl ConversationEditor {
.map(|summary| summary.text.clone()) .map(|summary| summary.text.clone())
.unwrap_or_else(|| "New Conversation".into()) .unwrap_or_else(|| "New Conversation".into())
} }
fn render_embedded_scope(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
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<MultiBuffer>,
cx: &mut ViewContext<Self>,
) -> 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<ConversationEditorEvent> for ConversationEditor {} impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
impl Render for ConversationEditor { impl Render for ConversationEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> 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() div()
.key_context("ConversationEditor") .key_context("ConversationEditor")
.capture_action(cx.listener(ConversationEditor::cancel_last_assist)) .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::assist))
.on_action(cx.listener(ConversationEditor::split)) .on_action(cx.listener(ConversationEditor::split))
.size_full() .size_full()
.relative() .v_flex()
.child( .child(
div() div()
.size_full() .flex_grow()
.pl_4() .pl_4()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.child(self.editor.clone()), .child(self.editor.clone()),
) )
.child(div().flex_shrink().children(self.render_embedded_scope(cx)))
} }
} }
@ -2799,8 +2965,9 @@ mod tests {
init(cx); init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = let conversation = cx.new_model(|cx| {
cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
});
let buffer = conversation.read(cx).buffer.clone(); let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone(); let message_1 = conversation.read(cx).message_anchors[0].clone();
@ -2931,8 +3098,9 @@ mod tests {
init(cx); init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = let conversation = cx.new_model(|cx| {
cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
});
let buffer = conversation.read(cx).buffer.clone(); let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone(); let message_1 = conversation.read(cx).message_anchors[0].clone();
@ -3030,8 +3198,9 @@ mod tests {
cx.set_global(settings_store); cx.set_global(settings_store);
init(cx); init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = let conversation = cx.new_model(|cx| {
cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx)); Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
});
let buffer = conversation.read(cx).buffer.clone(); let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].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.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
cx.update(init); cx.update(init);
let registry = Arc::new(LanguageRegistry::test(cx.executor())); let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = let conversation = cx.new_model(|cx| {
cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry.clone(), cx)); Conversation::new(
LanguageModel::default(),
registry.clone(),
EmbeddedScope::new(),
cx,
)
});
let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone()); let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
let message_0 = let message_0 =
conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id); conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);

View File

@ -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<Model<MultiBuffer>>,
active_buffer_enabled: bool,
active_buffer_subscription: Option<Subscription>,
}
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<Model<MultiBuffer>>,
cx: &mut ModelContext<Conversation>,
) {
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<MultiBuffer>> {
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<LanguageModelRequestMessage> {
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
}
}

View File

@ -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

View File

@ -12,13 +12,13 @@ struct TypeConfig {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct FileAssociations { pub struct FileIcons {
stems: HashMap<String, String>, stems: HashMap<String, String>,
suffixes: HashMap<String, String>, suffixes: HashMap<String, String>,
types: HashMap<String, TypeConfig>, types: HashMap<String, TypeConfig>,
} }
impl Global for FileAssociations {} impl Global for FileIcons {}
const COLLAPSED_DIRECTORY_TYPE: &str = "collapsed_folder"; const COLLAPSED_DIRECTORY_TYPE: &str = "collapsed_folder";
const EXPANDED_DIRECTORY_TYPE: &str = "expanded_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 const FILE_TYPES_ASSET: &str = "icons/file_icons/file_types.json";
pub fn init(assets: impl AssetSource, cx: &mut AppContext) { 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 { pub fn new(assets: impl AssetSource) -> Self {
assets assets
.load("icons/file_icons/file_types.json") .load("icons/file_icons/file_types.json")
.and_then(|file| { .and_then(|file| {
serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap()) serde_json::from_str::<FileIcons>(str::from_utf8(&file).unwrap())
.map_err(Into::into) .map_err(Into::into)
}) })
.unwrap_or_else(|_| FileAssociations { .unwrap_or_else(|_| FileIcons {
stems: HashMap::default(), stems: HashMap::default(),
suffixes: HashMap::default(), suffixes: HashMap::default(),
types: HashMap::default(), types: HashMap::default(),

View File

@ -486,6 +486,8 @@ pub struct CodeLabel {
pub struct LanguageConfig { pub struct LanguageConfig {
/// Human-readable name of the language. /// Human-readable name of the language.
pub name: Arc<str>, pub name: Arc<str>,
/// The name of this language for a Markdown code fence block
pub code_fence_block_name: Option<Arc<str>>,
// The name of the grammar in a WASM bundle (experimental). // The name of the grammar in a WASM bundle (experimental).
pub grammar: Option<Arc<str>>, pub grammar: Option<Arc<str>>,
/// The criteria for matching this language to a given file. /// The criteria for matching this language to a given file.
@ -609,6 +611,7 @@ impl Default for LanguageConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: "".into(), name: "".into(),
code_fence_block_name: None,
grammar: None, grammar: None,
matcher: LanguageMatcher::default(), matcher: LanguageMatcher::default(),
brackets: Default::default(), brackets: Default::default(),
@ -1185,6 +1188,13 @@ impl Language {
self.config.name.clone() self.config.name.clone()
} }
pub fn code_fence_block_name(&self) -> Arc<str> {
self.config
.code_fence_block_name
.clone()
.unwrap_or_else(|| self.config.name.to_lowercase().into())
}
pub fn context_provider(&self) -> Option<Arc<dyn ContextProvider>> { pub fn context_provider(&self) -> Option<Arc<dyn ContextProvider>> {
self.context_provider.clone() self.context_provider.clone()
} }

View File

@ -1,4 +1,5 @@
name = "Shell Script" name = "Shell Script"
code_fence_block_name = "bash"
grammar = "bash" grammar = "bash"
path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"] path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env"]
line_comments = ["# "] line_comments = ["# "]

View File

@ -1,4 +1,5 @@
name = "Go Mod" name = "Go Mod"
code_fence_block_name = "go.mod"
grammar = "gomod" grammar = "gomod"
path_suffixes = ["mod"] path_suffixes = ["mod"]
line_comments = ["//"] line_comments = ["//"]

View File

@ -1,4 +1,5 @@
name = "Go Work" name = "Go Work"
code_fence_block_name = "gowork"
grammar = "gowork" grammar = "gowork"
path_suffixes = ["work"] path_suffixes = ["work"]
line_comments = ["//"] line_comments = ["//"]

View File

@ -1,4 +1,5 @@
name = "OCaml Interface" name = "OCaml Interface"
code_fence_block_name = "ocaml"
grammar = "ocaml_interface" grammar = "ocaml_interface"
path_suffixes = ["mli"] path_suffixes = ["mli"]
block_comment = ["(* ", "*)"] block_comment = ["(* ", "*)"]

View File

@ -1,4 +1,5 @@
name = "Vue.js" name = "Vue.js"
code_fence_block_name = "vue"
grammar = "vue" grammar = "vue"
path_suffixes = ["vue"] path_suffixes = ["vue"]
block_comment = ["<!-- ", " -->"] block_comment = ["<!-- ", " -->"]

View File

@ -17,6 +17,7 @@ anyhow.workspace = true
collections.workspace = true collections.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
file_icons.workspace = true
gpui.workspace = true gpui.workspace = true
menu.workspace = true menu.workspace = true
pretty_assertions.workspace = true pretty_assertions.workspace = true

View File

@ -1,11 +1,10 @@
pub mod file_associations;
mod project_panel_settings; mod project_panel_settings;
use client::{ErrorCode, ErrorExt}; use client::{ErrorCode, ErrorExt};
use settings::Settings; use settings::Settings;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor}; 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 anyhow::{anyhow, Result};
use collections::{hash_map, HashMap}; 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) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
init_settings(cx); init_settings(cx);
file_associations::init(assets, cx); file_icons::init(assets, cx);
cx.observe_new_views(|workspace: &mut Workspace, _| { cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.register_action(|workspace, _: &ToggleFocus, cx| {
@ -229,7 +228,7 @@ impl ProjectPanel {
}) })
.detach(); .detach();
cx.observe_global::<FileAssociations>(|_, cx| { cx.observe_global::<FileIcons>(|_, cx| {
cx.notify(); cx.notify();
}) })
.detach(); .detach();
@ -1329,16 +1328,16 @@ impl ProjectPanel {
let icon = match entry.kind { let icon = match entry.kind {
EntryKind::File(_) => { EntryKind::File(_) => {
if show_file_icons { if show_file_icons {
FileAssociations::get_icon(&entry.path, cx) FileIcons::get_icon(&entry.path, cx)
} else { } else {
None None
} }
} }
_ => { _ => {
if show_folder_icons { if show_folder_icons {
FileAssociations::get_folder_icon(is_expanded, cx) FileIcons::get_folder_icon(is_expanded, cx)
} else { } else {
FileAssociations::get_chevron_icon(is_expanded, cx) FileIcons::get_chevron_icon(is_expanded, cx)
} }
} }
}; };

View File

@ -517,6 +517,7 @@ impl DelayedDebouncedEditAction {
pub enum Event { pub enum Event {
PaneAdded(View<Pane>), PaneAdded(View<Pane>),
ActiveItemChanged,
ContactRequestedJoin(u64), ContactRequestedJoin(u64),
WorkspaceCreated(WeakView<Workspace>), WorkspaceCreated(WeakView<Workspace>),
SpawnTask(SpawnInTerminal), SpawnTask(SpawnInTerminal),
@ -2377,6 +2378,7 @@ impl Workspace {
self.update_window_edited(cx); self.update_window_edited(cx);
} }
pane::Event::RemoveItem { item_id } => { pane::Event::RemoveItem { item_id } => {
cx.emit(Event::ActiveItemChanged);
self.update_window_edited(cx); self.update_window_edited(cx);
if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
if entry.get().entity_id() == pane.entity_id() { if entry.get().entity_id() == pane.entity_id() {
@ -2747,10 +2749,12 @@ impl Workspace {
.any(|state| state.leader_id == peer_id) .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<Self>) {
cx.emit(Event::ActiveItemChanged);
let active_entry = self.active_project_path(cx); let active_entry = self.active_project_path(cx);
self.project self.project
.update(cx, |project, cx| project.set_active_path(active_entry, cx)); .update(cx, |project, cx| project.set_active_path(active_entry, cx));
self.update_window_title(cx); self.update_window_title(cx);
} }

View File

@ -42,6 +42,7 @@ env_logger.workspace = true
extension.workspace = true extension.workspace = true
extensions_ui.workspace = true extensions_ui.workspace = true
feedback.workspace = true feedback.workspace = true
file_icons.workspace = true
file_finder.workspace = true file_finder.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true

View File

@ -1082,7 +1082,7 @@ fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
while (events.next().await).is_some() { while (events.next().await).is_some() {
cx.update(|cx| { cx.update(|cx| {
cx.update_global(|file_types, _| { cx.update_global(|file_types, _| {
*file_types = project_panel::file_associations::FileAssociations::new(Assets); *file_types = file_icons::FileIcons::new(Assets);
}); });
}) })
.ok(); .ok();

View File

@ -1,4 +1,5 @@
name = "CSharp" name = "CSharp"
code_fence_block_name = "csharp"
grammar = "c_sharp" grammar = "c_sharp"
path_suffixes = ["cs"] path_suffixes = ["cs"]
line_comments = ["// "] line_comments = ["// "]