mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00:00
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:
parent
df3050dac1
commit
d77e553466
20 changed files with 377 additions and 49 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
91
crates/assistant/src/embedded_scope.rs
Normal file
91
crates/assistant/src/embedded_scope.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
21
crates/file_icons/Cargo.toml
Normal file
21
crates/file_icons/Cargo.toml
Normal 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
|
|
@ -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(),
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = ["# "]
|
||||||
|
|
|
@ -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 = ["//"]
|
||||||
|
|
|
@ -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 = ["//"]
|
||||||
|
|
|
@ -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 = ["(* ", "*)"]
|
||||||
|
|
|
@ -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 = ["<!-- ", " -->"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 = ["// "]
|
||||||
|
|
Loading…
Reference in a new issue