mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 20:32:22 +03:00
Polish prompt library UX (#12647)
This could still use some improvement UI-wise but the user experience should be a lot better. - [x] Show in "Window" application menu - [x] Load prompt as it's selected in the picker - [x] Refocus picker on `esc` - [x] When creating a new prompt, if a new prompt already exists and is unedited, activate it instead - [x] Add `/default` command - [x] Evaluate /commands on prompt insertion - [x] Autocomplete /commands (but don't evaluate) during prompt editing - [x] Show token count using the settings model, right-aligned in the editor - [x] Picker - [x] Sorted alpha - [x] 2 sublists - Default - Empty state: Star a prompt to add it to your default prompt - Otherwise show prompts with star on hover - All - Move prompts with star on hover Release Notes: - N/A
This commit is contained in:
parent
e4bb666eab
commit
c5b22eee2d
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -339,7 +339,6 @@ dependencies = [
|
|||||||
"anthropic",
|
"anthropic",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
"async-watch",
|
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
"client",
|
||||||
@ -824,15 +823,6 @@ dependencies = [
|
|||||||
"tungstenite 0.16.0",
|
"tungstenite 0.16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-watch"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a078faf4e27c0c6cc0efb20e5da59dcccc04968ebf2801d8e0b2195124cdcdb2"
|
|
||||||
dependencies = [
|
|
||||||
"event-listener 2.5.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async_zip"
|
name = "async_zip"
|
||||||
version = "0.0.17"
|
version = "0.0.17"
|
||||||
|
5
assets/icons/zed_assistant_filled.svg
Normal file
5
assets/icons/zed_assistant_filled.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1017 B |
@ -16,7 +16,6 @@ doctest = false
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
anthropic = { workspace = true, features = ["schemars"] }
|
anthropic = { workspace = true, features = ["schemars"] }
|
||||||
assistant_slash_command.workspace = true
|
assistant_slash_command.workspace = true
|
||||||
async-watch.workspace = true
|
|
||||||
cargo_toml.workspace = true
|
cargo_toml.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
@ -19,14 +19,13 @@ use command_palette_hooks::CommandPaletteFilter;
|
|||||||
pub(crate) use completion_provider::*;
|
pub(crate) use completion_provider::*;
|
||||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||||
pub(crate) use model_selector::*;
|
pub(crate) use model_selector::*;
|
||||||
use prompt_library::PromptStore;
|
|
||||||
pub(crate) use saved_conversation::*;
|
pub(crate) use saved_conversation::*;
|
||||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use slash_command::{
|
use slash_command::{
|
||||||
active_command, fetch_command, file_command, project_command, prompt_command, rustdoc_command,
|
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
|
||||||
search_command, tabs_command,
|
rustdoc_command, search_command, tabs_command,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
@ -303,18 +302,10 @@ fn register_slash_commands(cx: &mut AppContext) {
|
|||||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||||
|
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||||
|
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||||
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||||
|
|
||||||
let store = PromptStore::global(cx);
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
let store = store.await?;
|
|
||||||
slash_command_registry
|
|
||||||
.register_command(prompt_command::PromptSlashCommand::new(store), true);
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||||
codegen::{self, Codegen, CodegenKind},
|
codegen::{self, Codegen, CodegenKind},
|
||||||
prompt_library::{open_prompt_library, PromptMetadata, PromptStore},
|
prompt_library::open_prompt_library,
|
||||||
prompts::generate_content_prompt,
|
prompts::generate_content_prompt,
|
||||||
search::*,
|
search::*,
|
||||||
slash_command::{
|
slash_command::{
|
||||||
prompt_command::PromptPlaceholder, SlashCommandCompletionProvider, SlashCommandLine,
|
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry,
|
||||||
},
|
},
|
||||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
||||||
@ -14,7 +14,7 @@ use crate::{
|
|||||||
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
|
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
||||||
use editor::{actions::ShowCompletions, GutterDimensions};
|
use editor::{actions::ShowCompletions, GutterDimensions};
|
||||||
@ -40,10 +40,9 @@ use gpui::{
|
|||||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||||
};
|
};
|
||||||
use language::LspAdapterDelegate;
|
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||||
OffsetRangeExt as _, Point, ToOffset as _,
|
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@ -118,14 +117,6 @@ pub struct AssistantPanel {
|
|||||||
_watch_saved_conversations: Task<Result<()>>,
|
_watch_saved_conversations: Task<Result<()>>,
|
||||||
authentication_prompt: Option<AnyView>,
|
authentication_prompt: Option<AnyView>,
|
||||||
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||||
default_prompt: DefaultPrompt,
|
|
||||||
_watch_prompt_store: Task<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct DefaultPrompt {
|
|
||||||
text: String,
|
|
||||||
sections: Vec<SlashCommandOutputSection<usize>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActiveConversationEditor {
|
struct ActiveConversationEditor {
|
||||||
@ -146,8 +137,6 @@ impl AssistantPanel {
|
|||||||
.await
|
.await
|
||||||
.log_err()
|
.log_err()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
|
|
||||||
let default_prompts = prompt_store.load_default().await?;
|
|
||||||
|
|
||||||
// TODO: deserialize state.
|
// TODO: deserialize state.
|
||||||
let workspace_handle = workspace.clone();
|
let workspace_handle = workspace.clone();
|
||||||
@ -173,22 +162,6 @@ impl AssistantPanel {
|
|||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
let _watch_prompt_store = cx.spawn(|this, mut cx| async move {
|
|
||||||
let mut updates = prompt_store.updates();
|
|
||||||
while updates.changed().await.is_ok() {
|
|
||||||
let Some(prompts) = prompt_store.load_default().await.log_err() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if this
|
|
||||||
.update(&mut cx, |this, _cx| this.update_default_prompt(prompts))
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let toolbar = cx.new_view(|cx| {
|
let toolbar = cx.new_view(|cx| {
|
||||||
let mut toolbar = Toolbar::new();
|
let mut toolbar = Toolbar::new();
|
||||||
toolbar.set_can_navigate(false, cx);
|
toolbar.set_can_navigate(false, cx);
|
||||||
@ -216,7 +189,7 @@ impl AssistantPanel {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut this = Self {
|
Self {
|
||||||
workspace: workspace_handle,
|
workspace: workspace_handle,
|
||||||
active_conversation_editor: None,
|
active_conversation_editor: None,
|
||||||
show_saved_conversations: false,
|
show_saved_conversations: false,
|
||||||
@ -239,11 +212,7 @@ impl AssistantPanel {
|
|||||||
_watch_saved_conversations,
|
_watch_saved_conversations,
|
||||||
authentication_prompt: None,
|
authentication_prompt: None,
|
||||||
model_menu_handle: PopoverMenuHandle::default(),
|
model_menu_handle: PopoverMenuHandle::default(),
|
||||||
default_prompt: DefaultPrompt::default(),
|
}
|
||||||
_watch_prompt_store,
|
|
||||||
};
|
|
||||||
this.update_default_prompt(default_prompts);
|
|
||||||
this
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -266,55 +235,6 @@ impl AssistantPanel {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_default_prompt(&mut self, prompts: Vec<(PromptMetadata, String)>) {
|
|
||||||
self.default_prompt.text.clear();
|
|
||||||
self.default_prompt.sections.clear();
|
|
||||||
if !prompts.is_empty() {
|
|
||||||
self.default_prompt.text.push_str("Default Prompt:\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (metadata, body) in prompts {
|
|
||||||
let section_start = self.default_prompt.text.len();
|
|
||||||
self.default_prompt.text.push_str(&body);
|
|
||||||
let section_end = self.default_prompt.text.len();
|
|
||||||
self.default_prompt
|
|
||||||
.sections
|
|
||||||
.push(SlashCommandOutputSection {
|
|
||||||
range: section_start..section_end,
|
|
||||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
|
||||||
PromptPlaceholder {
|
|
||||||
title: metadata
|
|
||||||
.title
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| SharedString::from("Untitled")),
|
|
||||||
id,
|
|
||||||
unfold,
|
|
||||||
}
|
|
||||||
.into_any_element()
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
self.default_prompt.text.push('\n');
|
|
||||||
}
|
|
||||||
self.default_prompt.text.pop();
|
|
||||||
|
|
||||||
if !self.default_prompt.text.is_empty() {
|
|
||||||
self.default_prompt.sections.insert(
|
|
||||||
0,
|
|
||||||
SlashCommandOutputSection {
|
|
||||||
range: 0..self.default_prompt.text.len(),
|
|
||||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
|
||||||
PromptPlaceholder {
|
|
||||||
title: "Default".into(),
|
|
||||||
id,
|
|
||||||
unfold,
|
|
||||||
}
|
|
||||||
.into_any_element()
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn completion_provider_changed(
|
fn completion_provider_changed(
|
||||||
&mut self,
|
&mut self,
|
||||||
prev_settings_version: usize,
|
prev_settings_version: usize,
|
||||||
@ -862,7 +782,6 @@ impl AssistantPanel {
|
|||||||
|
|
||||||
let editor = cx.new_view(|cx| {
|
let editor = cx.new_view(|cx| {
|
||||||
ConversationEditor::new(
|
ConversationEditor::new(
|
||||||
&self.default_prompt,
|
|
||||||
self.languages.clone(),
|
self.languages.clone(),
|
||||||
self.slash_commands.clone(),
|
self.slash_commands.clone(),
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
@ -1460,7 +1379,9 @@ enum ConversationEvent {
|
|||||||
updated: Vec<PendingSlashCommand>,
|
updated: Vec<PendingSlashCommand>,
|
||||||
},
|
},
|
||||||
SlashCommandFinished {
|
SlashCommandFinished {
|
||||||
|
output_range: Range<language::Anchor>,
|
||||||
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||||
|
run_commands_in_output: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1727,18 +1648,7 @@ impl Conversation {
|
|||||||
buffer.line_len(row_range.end - 1),
|
buffer.line_len(row_range.end - 1),
|
||||||
));
|
));
|
||||||
|
|
||||||
let start_ix = match self
|
let old_range = self.pending_command_indices_for_range(start..end, cx);
|
||||||
.pending_slash_commands
|
|
||||||
.binary_search_by(|probe| probe.source_range.start.cmp(&start, buffer))
|
|
||||||
{
|
|
||||||
Ok(ix) | Err(ix) => ix,
|
|
||||||
};
|
|
||||||
let end_ix = match self.pending_slash_commands[start_ix..]
|
|
||||||
.binary_search_by(|probe| probe.source_range.end.cmp(&end, buffer))
|
|
||||||
{
|
|
||||||
Ok(ix) => start_ix + ix + 1,
|
|
||||||
Err(ix) => start_ix + ix,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_commands = Vec::new();
|
let mut new_commands = Vec::new();
|
||||||
let mut lines = buffer.text_for_range(start..end).lines();
|
let mut lines = buffer.text_for_range(start..end).lines();
|
||||||
@ -1773,9 +1683,7 @@ impl Conversation {
|
|||||||
offset = lines.offset();
|
offset = lines.offset();
|
||||||
}
|
}
|
||||||
|
|
||||||
let removed_commands = self
|
let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
|
||||||
.pending_slash_commands
|
|
||||||
.splice(start_ix..end_ix, new_commands);
|
|
||||||
removed.extend(removed_commands.map(|command| command.source_range));
|
removed.extend(removed_commands.map(|command| command.source_range));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1849,25 +1757,60 @@ impl Conversation {
|
|||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<&mut PendingSlashCommand> {
|
) -> Option<&mut PendingSlashCommand> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let ix = self
|
match self
|
||||||
.pending_slash_commands
|
.pending_slash_commands
|
||||||
.binary_search_by(|probe| {
|
.binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
|
||||||
if probe.source_range.start.cmp(&position, buffer).is_gt() {
|
{
|
||||||
Ordering::Less
|
Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
|
||||||
} else if probe.source_range.end.cmp(&position, buffer).is_lt() {
|
Err(ix) => {
|
||||||
Ordering::Greater
|
let cmd = self.pending_slash_commands.get_mut(ix)?;
|
||||||
|
if position.cmp(&cmd.source_range.start, buffer).is_ge()
|
||||||
|
&& position.cmp(&cmd.source_range.end, buffer).is_le()
|
||||||
|
{
|
||||||
|
Some(cmd)
|
||||||
} else {
|
} else {
|
||||||
Ordering::Equal
|
None
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.ok()?;
|
}
|
||||||
self.pending_slash_commands.get_mut(ix)
|
}
|
||||||
|
|
||||||
|
fn pending_commands_for_range(
|
||||||
|
&self,
|
||||||
|
range: Range<language::Anchor>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> &[PendingSlashCommand] {
|
||||||
|
let range = self.pending_command_indices_for_range(range, cx);
|
||||||
|
&self.pending_slash_commands[range]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_command_indices_for_range(
|
||||||
|
&self,
|
||||||
|
range: Range<language::Anchor>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Range<usize> {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
let start_ix = match self
|
||||||
|
.pending_slash_commands
|
||||||
|
.binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
|
||||||
|
{
|
||||||
|
Ok(ix) | Err(ix) => ix,
|
||||||
|
};
|
||||||
|
let end_ix = match self
|
||||||
|
.pending_slash_commands
|
||||||
|
.binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
|
||||||
|
{
|
||||||
|
Ok(ix) => ix + 1,
|
||||||
|
Err(ix) => ix,
|
||||||
|
};
|
||||||
|
start_ix..end_ix
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_command_output(
|
fn insert_command_output(
|
||||||
&mut self,
|
&mut self,
|
||||||
command_range: Range<language::Anchor>,
|
command_range: Range<language::Anchor>,
|
||||||
output: Task<Result<SlashCommandOutput>>,
|
output: Task<Result<SlashCommandOutput>>,
|
||||||
|
insert_trailing_newline: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.reparse_slash_commands(cx);
|
self.reparse_slash_commands(cx);
|
||||||
@ -1878,13 +1821,14 @@ impl Conversation {
|
|||||||
let output = output.await;
|
let output = output.await;
|
||||||
this.update(&mut cx, |this, cx| match output {
|
this.update(&mut cx, |this, cx| match output {
|
||||||
Ok(mut output) => {
|
Ok(mut output) => {
|
||||||
if !output.text.ends_with('\n') {
|
if insert_trailing_newline {
|
||||||
output.text.push('\n');
|
output.text.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
let sections = this.buffer.update(cx, |buffer, cx| {
|
let event = this.buffer.update(cx, |buffer, cx| {
|
||||||
let start = command_range.start.to_offset(buffer);
|
let start = command_range.start.to_offset(buffer);
|
||||||
let old_end = command_range.end.to_offset(buffer);
|
let old_end = command_range.end.to_offset(buffer);
|
||||||
|
let new_end = start + output.text.len();
|
||||||
buffer.edit([(start..old_end, output.text)], None, cx);
|
buffer.edit([(start..old_end, output.text)], None, cx);
|
||||||
|
|
||||||
let mut sections = output
|
let mut sections = output
|
||||||
@ -1897,9 +1841,14 @@ impl Conversation {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
||||||
sections
|
ConversationEvent::SlashCommandFinished {
|
||||||
|
output_range: buffer.anchor_after(start)
|
||||||
|
..buffer.anchor_before(new_end),
|
||||||
|
sections,
|
||||||
|
run_commands_in_output: output.run_commands_in_text,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cx.emit(ConversationEvent::SlashCommandFinished { sections });
|
cx.emit(event);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if let Some(pending_command) =
|
if let Some(pending_command) =
|
||||||
@ -2596,7 +2545,6 @@ pub struct ConversationEditor {
|
|||||||
|
|
||||||
impl ConversationEditor {
|
impl ConversationEditor {
|
||||||
fn new(
|
fn new(
|
||||||
default_prompt: &DefaultPrompt,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
@ -2618,31 +2566,7 @@ impl ConversationEditor {
|
|||||||
|
|
||||||
let mut this =
|
let mut this =
|
||||||
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx);
|
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx);
|
||||||
|
this.insert_default_prompt(cx);
|
||||||
if !default_prompt.text.is_empty() {
|
|
||||||
this.editor
|
|
||||||
.update(cx, |editor, cx| editor.insert(&default_prompt.text, cx));
|
|
||||||
let snapshot = this.conversation.read(cx).buffer.read(cx).text_snapshot();
|
|
||||||
this.insert_slash_command_output_sections(
|
|
||||||
default_prompt
|
|
||||||
.sections
|
|
||||||
.iter()
|
|
||||||
.map(|section| SlashCommandOutputSection {
|
|
||||||
range: snapshot.anchor_after(section.range.start)
|
|
||||||
..snapshot.anchor_before(section.range.end),
|
|
||||||
render_placeholder: section.render_placeholder.clone(),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
this.split(&Split, cx);
|
|
||||||
this.conversation.update(cx, |this, _cx| {
|
|
||||||
this.messages_metadata
|
|
||||||
.get_mut(&MessageId::default())
|
|
||||||
.unwrap()
|
|
||||||
.role = Role::System;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2695,6 +2619,32 @@ impl ConversationEditor {
|
|||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let command_name = DefaultSlashCommand.name();
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.insert(&format!("/{command_name}"), cx)
|
||||||
|
});
|
||||||
|
self.split(&Split, cx);
|
||||||
|
let command = self.conversation.update(cx, |conversation, cx| {
|
||||||
|
conversation
|
||||||
|
.messages_metadata
|
||||||
|
.get_mut(&MessageId::default())
|
||||||
|
.unwrap()
|
||||||
|
.role = Role::System;
|
||||||
|
conversation.reparse_slash_commands(cx);
|
||||||
|
conversation.pending_slash_commands[0].clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
self.run_command(
|
||||||
|
command.source_range,
|
||||||
|
&command.name,
|
||||||
|
command.argument.as_deref(),
|
||||||
|
false,
|
||||||
|
self.workspace.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
||||||
let cursors = self.cursors(cx);
|
let cursors = self.cursors(cx);
|
||||||
|
|
||||||
@ -2817,6 +2767,7 @@ impl ConversationEditor {
|
|||||||
command.source_range,
|
command.source_range,
|
||||||
&command.name,
|
&command.name,
|
||||||
command.argument.as_deref(),
|
command.argument.as_deref(),
|
||||||
|
true,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@ -2830,6 +2781,7 @@ impl ConversationEditor {
|
|||||||
command_range: Range<language::Anchor>,
|
command_range: Range<language::Anchor>,
|
||||||
name: &str,
|
name: &str,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
|
insert_trailing_newline: bool,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
@ -2838,7 +2790,12 @@ impl ConversationEditor {
|
|||||||
let argument = argument.map(ToString::to_string);
|
let argument = argument.map(ToString::to_string);
|
||||||
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
|
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
|
||||||
self.conversation.update(cx, |conversation, cx| {
|
self.conversation.update(cx, |conversation, cx| {
|
||||||
conversation.insert_command_output(command_range, output, cx)
|
conversation.insert_command_output(
|
||||||
|
command_range,
|
||||||
|
output,
|
||||||
|
insert_trailing_newline,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2938,6 +2895,7 @@ impl ConversationEditor {
|
|||||||
command.source_range.clone(),
|
command.source_range.clone(),
|
||||||
&command.name,
|
&command.name,
|
||||||
command.argument.as_deref(),
|
command.argument.as_deref(),
|
||||||
|
false,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@ -2991,8 +2949,32 @@ impl ConversationEditor {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ConversationEvent::SlashCommandFinished { sections } => {
|
ConversationEvent::SlashCommandFinished {
|
||||||
|
output_range,
|
||||||
|
sections,
|
||||||
|
run_commands_in_output,
|
||||||
|
} => {
|
||||||
self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
|
self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
|
||||||
|
|
||||||
|
if *run_commands_in_output {
|
||||||
|
let commands = self.conversation.update(cx, |conversation, cx| {
|
||||||
|
conversation.reparse_slash_commands(cx);
|
||||||
|
conversation
|
||||||
|
.pending_commands_for_range(output_range.clone(), cx)
|
||||||
|
.to_vec()
|
||||||
|
});
|
||||||
|
|
||||||
|
for command in commands {
|
||||||
|
self.run_command(
|
||||||
|
command.source_range,
|
||||||
|
&command.name,
|
||||||
|
command.argument.as_deref(),
|
||||||
|
false,
|
||||||
|
self.workspace.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,40 @@
|
|||||||
|
use crate::{
|
||||||
|
slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest,
|
||||||
|
LanguageModelRequestMessage, Role,
|
||||||
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, EditorEvent};
|
use editor::{actions::Tab, Editor, EditorEvent};
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{self, BoxFuture, Shared},
|
future::{self, BoxFuture, Shared},
|
||||||
FutureExt,
|
FutureExt,
|
||||||
};
|
};
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, point, size, AppContext, BackgroundExecutor, Bounds, DevicePixels, Empty,
|
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
|
EventEmitter, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||||
WindowBounds, WindowHandle, WindowOptions,
|
View, WindowBounds, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use heed::{types::SerdeBincode, Database, RoTxn};
|
use heed::{types::SerdeBincode, Database, RoTxn};
|
||||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
|
use language::{
|
||||||
|
language_settings::SoftWrap, Buffer, Documentation, LanguageRegistry, LanguageServerId, Point,
|
||||||
|
ToPoint as _,
|
||||||
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
|
||||||
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||||
};
|
};
|
||||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -80,7 +87,7 @@ pub fn open_prompt_library(
|
|||||||
cx.open_window(
|
cx.open_window(
|
||||||
WindowOptions {
|
WindowOptions {
|
||||||
titlebar: Some(TitlebarOptions {
|
titlebar: Some(TitlebarOptions {
|
||||||
title: None,
|
title: Some("Prompt Library".into()),
|
||||||
appears_transparent: true,
|
appears_transparent: true,
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
}),
|
}),
|
||||||
@ -106,6 +113,8 @@ pub struct PromptLibrary {
|
|||||||
|
|
||||||
struct PromptEditor {
|
struct PromptEditor {
|
||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
|
token_count: Option<usize>,
|
||||||
|
pending_token_count: Task<Option<()>>,
|
||||||
next_body_to_save: Option<Rope>,
|
next_body_to_save: Option<Rope>,
|
||||||
pending_save: Option<Task<Option<()>>>,
|
pending_save: Option<Task<Option<()>>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
@ -114,30 +123,54 @@ struct PromptEditor {
|
|||||||
struct PromptPickerDelegate {
|
struct PromptPickerDelegate {
|
||||||
store: Arc<PromptStore>,
|
store: Arc<PromptStore>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
matches: Vec<PromptMetadata>,
|
entries: Vec<PromptPickerEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PromptPickerEvent {
|
enum PromptPickerEvent {
|
||||||
|
Selected { prompt_id: PromptId },
|
||||||
Confirmed { prompt_id: PromptId },
|
Confirmed { prompt_id: PromptId },
|
||||||
Deleted { prompt_id: PromptId },
|
Deleted { prompt_id: PromptId },
|
||||||
ToggledDefault { prompt_id: PromptId },
|
ToggledDefault { prompt_id: PromptId },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum PromptPickerEntry {
|
||||||
|
DefaultPromptsHeader,
|
||||||
|
DefaultPromptsEmpty,
|
||||||
|
AllPromptsHeader,
|
||||||
|
AllPromptsEmpty,
|
||||||
|
Prompt(PromptMetadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptPickerEntry {
|
||||||
|
fn prompt_id(&self) -> Option<PromptId> {
|
||||||
|
match self {
|
||||||
|
PromptPickerEntry::Prompt(metadata) => Some(metadata.id),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
|
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
|
||||||
|
|
||||||
impl PickerDelegate for PromptPickerDelegate {
|
impl PickerDelegate for PromptPickerDelegate {
|
||||||
type ListItem = ListItem;
|
type ListItem = AnyElement;
|
||||||
|
|
||||||
fn match_count(&self) -> usize {
|
fn match_count(&self) -> usize {
|
||||||
self.matches.len()
|
self.entries.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selected_index(&self) -> usize {
|
fn selected_index(&self) -> usize {
|
||||||
self.selected_index
|
self.selected_index
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.selected_index = ix;
|
self.selected_index = ix;
|
||||||
|
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
|
||||||
|
cx.emit(PromptPickerEvent::Selected {
|
||||||
|
prompt_id: prompt.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||||
@ -146,11 +179,49 @@ impl PickerDelegate for PromptPickerDelegate {
|
|||||||
|
|
||||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||||
let search = self.store.search(query);
|
let search = self.store.search(query);
|
||||||
|
let prev_prompt_id = self
|
||||||
|
.entries
|
||||||
|
.get(self.selected_index)
|
||||||
|
.and_then(|mat| mat.prompt_id());
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let matches = search.await;
|
let (entries, selected_index) = cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let prompts = search.await;
|
||||||
|
let (default_prompts, prompts) = prompts
|
||||||
|
.into_iter()
|
||||||
|
.partition::<Vec<_>, _>(|prompt| prompt.default);
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
entries.push(PromptPickerEntry::DefaultPromptsHeader);
|
||||||
|
if default_prompts.is_empty() {
|
||||||
|
entries.push(PromptPickerEntry::DefaultPromptsEmpty);
|
||||||
|
} else {
|
||||||
|
entries.extend(default_prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(PromptPickerEntry::AllPromptsHeader);
|
||||||
|
if prompts.is_empty() {
|
||||||
|
entries.push(PromptPickerEntry::AllPromptsEmpty);
|
||||||
|
} else {
|
||||||
|
entries.extend(prompts.into_iter().map(PromptPickerEntry::Prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected_index = prev_prompt_id
|
||||||
|
.and_then(|prev_prompt_id| {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.prompt_id() == Some(prev_prompt_id))
|
||||||
|
})
|
||||||
|
.or_else(|| entries.iter().position(|entry| entry.prompt_id().is_some()))
|
||||||
|
.unwrap_or(0);
|
||||||
|
(entries, selected_index)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.delegate.selected_index = 0;
|
this.delegate.entries = entries;
|
||||||
this.delegate.matches = matches;
|
this.delegate.set_selected_index(selected_index, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
@ -158,7 +229,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
|
||||||
cx.emit(PromptPickerEvent::Confirmed {
|
cx.emit(PromptPickerEvent::Confirmed {
|
||||||
prompt_id: prompt.id,
|
prompt_id: prompt.id,
|
||||||
});
|
});
|
||||||
@ -173,61 +244,80 @@ impl PickerDelegate for PromptPickerDelegate {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let prompt = self.matches.get(ix)?;
|
let prompt = self.entries.get(ix)?;
|
||||||
let default = prompt.default;
|
let element = match prompt {
|
||||||
let prompt_id = prompt.id;
|
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
|
||||||
Some(
|
|
||||||
ListItem::new(ix)
|
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.start_slot(Icon::new(IconName::ZedAssistant))
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.child(Label::new(
|
.into_any_element(),
|
||||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
PromptPickerEntry::DefaultPromptsEmpty => {
|
||||||
))
|
ListSubHeader::new("Star a prompt to add it to the default context")
|
||||||
.end_slot(if default {
|
.inset(true)
|
||||||
IconButton::new("toggle-default-prompt", IconName::StarFilled)
|
.selected(selected)
|
||||||
.shape(IconButtonShape::Square)
|
.into_any_element()
|
||||||
.into_any_element()
|
}
|
||||||
} else {
|
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
|
||||||
Empty.into_any()
|
.inset(true)
|
||||||
})
|
.start_slot(Icon::new(IconName::Library))
|
||||||
.end_hover_slot(
|
.selected(selected)
|
||||||
h_flex()
|
.into_any_element(),
|
||||||
.gap_2()
|
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
|
||||||
.child(
|
.inset(true)
|
||||||
IconButton::new("delete-prompt", IconName::Trash)
|
.selected(selected)
|
||||||
.shape(IconButtonShape::Square)
|
.into_any_element(),
|
||||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
PromptPickerEntry::Prompt(prompt) => {
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
let default = prompt.default;
|
||||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
let prompt_id = prompt.id;
|
||||||
})),
|
ListItem::new(ix)
|
||||||
)
|
.inset(true)
|
||||||
.child(
|
.spacing(ListItemSpacing::Sparse)
|
||||||
IconButton::new(
|
.selected(selected)
|
||||||
"toggle-default-prompt",
|
.child(Label::new(
|
||||||
if default {
|
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||||
IconName::StarFilled
|
))
|
||||||
} else {
|
.end_hover_slot(
|
||||||
IconName::Star
|
h_flex()
|
||||||
},
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
IconButton::new("delete-prompt", IconName::Trash)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||||
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
|
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.shape(IconButtonShape::Square)
|
.child(
|
||||||
.tooltip(move |cx| {
|
IconButton::new(
|
||||||
Tooltip::text(
|
"toggle-default-prompt",
|
||||||
if default {
|
if default {
|
||||||
"Remove from Default Prompt"
|
IconName::ZedAssistantFilled
|
||||||
} else {
|
} else {
|
||||||
"Add to Default Prompt"
|
IconName::ZedAssistant
|
||||||
},
|
},
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
})
|
.shape(IconButtonShape::Square)
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
.tooltip(move |cx| {
|
||||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
Tooltip::text(
|
||||||
})),
|
if default {
|
||||||
),
|
"Remove from Default Prompt"
|
||||||
),
|
} else {
|
||||||
)
|
"Add to Default Prompt"
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click(cx.listener(
|
||||||
|
move |_, _, cx| {
|
||||||
|
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,17 +330,15 @@ impl PromptLibrary {
|
|||||||
let delegate = PromptPickerDelegate {
|
let delegate = PromptPickerDelegate {
|
||||||
store: store.clone(),
|
store: store.clone(),
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
matches: Vec::new(),
|
entries: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let picker = cx.new_view(|cx| {
|
let picker = cx.new_view(|cx| {
|
||||||
let picker = Picker::uniform_list(delegate, cx)
|
let picker = Picker::list(delegate, cx).modal(false).max_height(None);
|
||||||
.modal(false)
|
|
||||||
.max_height(None);
|
|
||||||
picker.focus(cx);
|
picker.focus(cx);
|
||||||
picker
|
picker
|
||||||
});
|
});
|
||||||
let mut this = Self {
|
Self {
|
||||||
store: store.clone(),
|
store: store.clone(),
|
||||||
language_registry,
|
language_registry,
|
||||||
prompt_editors: HashMap::default(),
|
prompt_editors: HashMap::default(),
|
||||||
@ -258,11 +346,7 @@ impl PromptLibrary {
|
|||||||
pending_load: Task::ready(()),
|
pending_load: Task::ready(()),
|
||||||
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
|
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
|
||||||
picker,
|
picker,
|
||||||
};
|
|
||||||
if let Some(prompt_id) = store.most_recently_saved() {
|
|
||||||
this.load_prompt(prompt_id, false, cx);
|
|
||||||
}
|
}
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_picker_event(
|
fn handle_picker_event(
|
||||||
@ -272,6 +356,9 @@ impl PromptLibrary {
|
|||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
|
PromptPickerEvent::Selected { prompt_id } => {
|
||||||
|
self.load_prompt(*prompt_id, false, cx);
|
||||||
|
}
|
||||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||||
self.load_prompt(*prompt_id, true, cx);
|
self.load_prompt(*prompt_id, true, cx);
|
||||||
}
|
}
|
||||||
@ -285,6 +372,15 @@ impl PromptLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
// If we already have an untitled prompt, use that instead
|
||||||
|
// of creating a new one.
|
||||||
|
if let Some(metadata) = self.store.first() {
|
||||||
|
if metadata.title.is_none() {
|
||||||
|
self.load_prompt(metadata.id, true, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let prompt_id = PromptId::new();
|
let prompt_id = PromptId::new();
|
||||||
let save = self.store.save(prompt_id, None, false, "".into());
|
let save = self.store.save(prompt_id, None, false, "".into());
|
||||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||||
@ -383,7 +479,7 @@ impl PromptLibrary {
|
|||||||
.editor
|
.editor
|
||||||
.update(cx, |editor, cx| editor.focus(cx));
|
.update(cx, |editor, cx| editor.focus(cx));
|
||||||
}
|
}
|
||||||
self.active_prompt_id = Some(prompt_id);
|
self.set_active_prompt(Some(prompt_id), cx);
|
||||||
} else {
|
} else {
|
||||||
let language_registry = self.language_registry.clone();
|
let language_registry = self.language_registry.clone();
|
||||||
let prompt = self.store.load(prompt_id);
|
let prompt = self.store.load(prompt_id);
|
||||||
@ -404,6 +500,8 @@ impl PromptLibrary {
|
|||||||
editor.set_show_gutter(false, cx);
|
editor.set_show_gutter(false, cx);
|
||||||
editor.set_show_wrap_guides(false, cx);
|
editor.set_show_wrap_guides(false, cx);
|
||||||
editor.set_show_indent_guides(false, cx);
|
editor.set_show_indent_guides(false, cx);
|
||||||
|
editor
|
||||||
|
.set_completion_provider(Box::new(SlashCommandCompletionProvider));
|
||||||
if focus {
|
if focus {
|
||||||
editor.focus(cx);
|
editor.focus(cx);
|
||||||
}
|
}
|
||||||
@ -419,11 +517,13 @@ impl PromptLibrary {
|
|||||||
editor,
|
editor,
|
||||||
next_body_to_save: None,
|
next_body_to_save: None,
|
||||||
pending_save: None,
|
pending_save: None,
|
||||||
|
token_count: None,
|
||||||
|
pending_token_count: Task::ready(None),
|
||||||
_subscription,
|
_subscription,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.active_prompt_id = Some(prompt_id);
|
this.set_active_prompt(Some(prompt_id), cx);
|
||||||
cx.notify();
|
this.count_tokens(prompt_id, cx);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
// TODO: we should show the error in the UI.
|
// TODO: we should show the error in the UI.
|
||||||
@ -435,6 +535,32 @@ impl PromptLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.active_prompt_id = prompt_id;
|
||||||
|
self.picker.update(cx, |picker, cx| {
|
||||||
|
if let Some(prompt_id) = prompt_id {
|
||||||
|
if picker
|
||||||
|
.delegate
|
||||||
|
.entries
|
||||||
|
.get(picker.delegate.selected_index())
|
||||||
|
.map_or(true, |old_selected_prompt| {
|
||||||
|
old_selected_prompt.prompt_id() != Some(prompt_id)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if let Some(ix) = picker
|
||||||
|
.delegate
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.position(|mat| mat.prompt_id() == Some(prompt_id))
|
||||||
|
{
|
||||||
|
picker.set_selected_index(ix, true, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||||
let confirmation = cx.prompt(
|
let confirmation = cx.prompt(
|
||||||
@ -451,7 +577,7 @@ impl PromptLibrary {
|
|||||||
if confirmation.await.ok() == Some(0) {
|
if confirmation.await.ok() == Some(0) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if this.active_prompt_id == Some(prompt_id) {
|
if this.active_prompt_id == Some(prompt_id) {
|
||||||
this.active_prompt_id = None;
|
this.set_active_prompt(None, cx);
|
||||||
}
|
}
|
||||||
this.prompt_editors.remove(&prompt_id);
|
this.prompt_editors.remove(&prompt_id);
|
||||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||||
@ -465,6 +591,19 @@ impl PromptLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(active_prompt) = self.active_prompt_id {
|
||||||
|
self.prompt_editors[&active_prompt]
|
||||||
|
.editor
|
||||||
|
.update(cx, |editor, cx| editor.focus(cx));
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_prompt_editor_event(
|
fn handle_prompt_editor_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt_id: PromptId,
|
prompt_id: PromptId,
|
||||||
@ -502,12 +641,53 @@ impl PromptLibrary {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.save_prompt(prompt_id, cx);
|
self.save_prompt(prompt_id, cx);
|
||||||
|
self.count_tokens(prompt_id, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
|
||||||
|
let editor = &prompt.editor.read(cx);
|
||||||
|
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||||
|
let body = buffer.as_rope().clone();
|
||||||
|
prompt.pending_token_count = cx.spawn(|this, mut cx| {
|
||||||
|
async move {
|
||||||
|
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||||
|
let token_count = cx
|
||||||
|
.update(|cx| {
|
||||||
|
let provider = CompletionProvider::global(cx);
|
||||||
|
let model = provider.model();
|
||||||
|
provider.count_tokens(
|
||||||
|
LanguageModelRequest {
|
||||||
|
model,
|
||||||
|
messages: vec![LanguageModelRequestMessage {
|
||||||
|
role: Role::System,
|
||||||
|
content: body.to_string(),
|
||||||
|
}],
|
||||||
|
stop: Vec::new(),
|
||||||
|
temperature: 1.,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||||
|
prompt_editor.token_count = Some(token_count);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("prompt-list")
|
.id("prompt-list")
|
||||||
|
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||||
.bg(cx.theme().colors().panel_background)
|
.bg(cx.theme().colors().panel_background)
|
||||||
.h_full()
|
.h_full()
|
||||||
.w_1_3()
|
.w_1_3()
|
||||||
@ -545,64 +725,69 @@ impl PromptLibrary {
|
|||||||
.min_w_64()
|
.min_w_64()
|
||||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||||
let editor = self.prompt_editors[&prompt_id].editor.clone();
|
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||||
Some(
|
Some(
|
||||||
v_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.items_start()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
div()
|
||||||
.h(TitleBar::height(cx))
|
.on_action(cx.listener(Self::focus_picker))
|
||||||
.px(Spacing::Large.rems(cx))
|
.flex_grow()
|
||||||
.justify_end()
|
.h_full()
|
||||||
.child(
|
.pt(Spacing::Large.rems(cx))
|
||||||
h_flex()
|
.pl(Spacing::Large.rems(cx))
|
||||||
.gap_4()
|
.child(prompt_editor.editor.clone()),
|
||||||
.child(
|
|
||||||
IconButton::new(
|
|
||||||
"toggle-default-prompt",
|
|
||||||
if prompt_metadata.default {
|
|
||||||
IconName::StarFilled
|
|
||||||
} else {
|
|
||||||
IconName::Star
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.shape(IconButtonShape::Square)
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
Tooltip::for_action(
|
|
||||||
if prompt_metadata.default {
|
|
||||||
"Remove from Default Prompt"
|
|
||||||
} else {
|
|
||||||
"Add to Default Prompt"
|
|
||||||
},
|
|
||||||
&ToggleDefaultPrompt,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click(
|
|
||||||
|_, cx| {
|
|
||||||
cx.dispatch_action(Box::new(
|
|
||||||
ToggleDefaultPrompt,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
IconButton::new("delete-prompt", IconName::Trash)
|
|
||||||
.shape(IconButtonShape::Square)
|
|
||||||
.tooltip(move |cx| {
|
|
||||||
Tooltip::for_action(
|
|
||||||
"Delete Prompt",
|
|
||||||
&DeletePrompt,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click(|_, cx| {
|
|
||||||
cx.dispatch_action(Box::new(DeletePrompt));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(div().flex_grow().p(Spacing::Large.rems(cx)).child(editor)),
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_12()
|
||||||
|
.py(Spacing::Large.rems(cx))
|
||||||
|
.justify_start()
|
||||||
|
.items_center()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
IconButton::new(
|
||||||
|
"toggle-default-prompt",
|
||||||
|
if prompt_metadata.default {
|
||||||
|
IconName::ZedAssistantFilled
|
||||||
|
} else {
|
||||||
|
IconName::ZedAssistant
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size(ButtonSize::Large)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::for_action(
|
||||||
|
if prompt_metadata.default {
|
||||||
|
"Remove from Default Prompt"
|
||||||
|
} else {
|
||||||
|
"Add to Default Prompt"
|
||||||
|
},
|
||||||
|
&ToggleDefaultPrompt,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("delete-prompt", IconName::Trash)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(move |cx| {
|
||||||
|
Tooltip::for_action("Delete Prompt", &DeletePrompt, cx)
|
||||||
|
})
|
||||||
|
.on_click(|_, cx| {
|
||||||
|
cx.dispatch_action(Box::new(DeletePrompt));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.children(prompt_editor.token_count.map(|token_count| {
|
||||||
|
h_flex()
|
||||||
|
.justify_center()
|
||||||
|
.child(Label::new(token_count.to_string()))
|
||||||
|
})),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -648,7 +833,6 @@ pub struct PromptStore {
|
|||||||
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
|
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
|
||||||
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||||
metadata_cache: RwLock<MetadataCache>,
|
metadata_cache: RwLock<MetadataCache>,
|
||||||
updates: (Arc<async_watch::Sender<()>>, async_watch::Receiver<()>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -668,9 +852,7 @@ impl MetadataCache {
|
|||||||
cache.metadata.push(metadata.clone());
|
cache.metadata.push(metadata.clone());
|
||||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||||
}
|
}
|
||||||
cache
|
cache.sort();
|
||||||
.metadata
|
|
||||||
.sort_unstable_by_key(|metadata| Reverse(metadata.saved_at));
|
|
||||||
Ok(cache)
|
Ok(cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,13 +863,21 @@ impl MetadataCache {
|
|||||||
} else {
|
} else {
|
||||||
self.metadata.push(metadata);
|
self.metadata.push(metadata);
|
||||||
}
|
}
|
||||||
self.metadata.sort_by_key(|m| Reverse(m.saved_at));
|
self.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove(&mut self, id: PromptId) {
|
fn remove(&mut self, id: PromptId) {
|
||||||
self.metadata.retain(|metadata| metadata.id != id);
|
self.metadata.retain(|metadata| metadata.id != id);
|
||||||
self.metadata_by_id.remove(&id);
|
self.metadata_by_id.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sort(&mut self) {
|
||||||
|
self.metadata.sort_unstable_by(|a, b| {
|
||||||
|
a.title
|
||||||
|
.cmp(&b.title)
|
||||||
|
.then_with(|| b.saved_at.cmp(&a.saved_at))
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PromptStore {
|
impl PromptStore {
|
||||||
@ -715,23 +905,17 @@ impl PromptStore {
|
|||||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
|
|
||||||
let (updates_tx, updates_rx) = async_watch::channel(());
|
|
||||||
Ok(PromptStore {
|
Ok(PromptStore {
|
||||||
executor,
|
executor,
|
||||||
env: db_env,
|
env: db_env,
|
||||||
bodies,
|
bodies,
|
||||||
metadata,
|
metadata,
|
||||||
metadata_cache: RwLock::new(metadata_cache),
|
metadata_cache: RwLock::new(metadata_cache),
|
||||||
updates: (Arc::new(updates_tx), updates_rx),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn updates(&self) -> async_watch::Receiver<()> {
|
|
||||||
self.updates.1.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
|
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
|
||||||
let env = self.env.clone();
|
let env = self.env.clone();
|
||||||
let bodies = self.bodies;
|
let bodies = self.bodies;
|
||||||
@ -743,8 +927,8 @@ impl PromptStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_default(&self) -> Task<Result<Vec<(PromptMetadata, String)>>> {
|
pub fn default_prompt_metadata(&self) -> Vec<PromptMetadata> {
|
||||||
let default_metadatas = self
|
return self
|
||||||
.metadata_cache
|
.metadata_cache
|
||||||
.read()
|
.read()
|
||||||
.metadata
|
.metadata
|
||||||
@ -752,23 +936,6 @@ impl PromptStore {
|
|||||||
.filter(|metadata| metadata.default)
|
.filter(|metadata| metadata.default)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let env = self.env.clone();
|
|
||||||
let bodies = self.bodies;
|
|
||||||
self.executor.spawn(async move {
|
|
||||||
let txn = env.read_txn()?;
|
|
||||||
|
|
||||||
let mut default_prompts = Vec::new();
|
|
||||||
for metadata in default_metadatas {
|
|
||||||
if let Some(body) = bodies.get(&txn, &metadata.id)? {
|
|
||||||
if !body.is_empty() {
|
|
||||||
default_prompts.push((metadata, body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default_prompts.sort_unstable_by_key(|(metadata, _)| metadata.saved_at);
|
|
||||||
Ok(default_prompts)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
|
pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
|
||||||
@ -854,7 +1021,6 @@ impl PromptStore {
|
|||||||
let db_connection = self.env.clone();
|
let db_connection = self.env.clone();
|
||||||
let bodies = self.bodies;
|
let bodies = self.bodies;
|
||||||
let metadata = self.metadata;
|
let metadata = self.metadata;
|
||||||
let updates = self.updates.0.clone();
|
|
||||||
|
|
||||||
self.executor.spawn(async move {
|
self.executor.spawn(async move {
|
||||||
let mut txn = db_connection.write_txn()?;
|
let mut txn = db_connection.write_txn()?;
|
||||||
@ -863,7 +1029,6 @@ impl PromptStore {
|
|||||||
bodies.put(&mut txn, &id, &body.to_string())?;
|
bodies.put(&mut txn, &id, &body.to_string())?;
|
||||||
|
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
updates.send(()).ok();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@ -885,24 +1050,18 @@ impl PromptStore {
|
|||||||
|
|
||||||
let db_connection = self.env.clone();
|
let db_connection = self.env.clone();
|
||||||
let metadata = self.metadata;
|
let metadata = self.metadata;
|
||||||
let updates = self.updates.0.clone();
|
|
||||||
|
|
||||||
self.executor.spawn(async move {
|
self.executor.spawn(async move {
|
||||||
let mut txn = db_connection.write_txn()?;
|
let mut txn = db_connection.write_txn()?;
|
||||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
updates.send(()).ok();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn most_recently_saved(&self) -> Option<PromptId> {
|
fn first(&self) -> Option<PromptMetadata> {
|
||||||
self.metadata_cache
|
self.metadata_cache.read().metadata.first().cloned()
|
||||||
.read()
|
|
||||||
.metadata
|
|
||||||
.first()
|
|
||||||
.map(|metadata| metadata.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -933,3 +1092,123 @@ fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString>
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SlashCommandCompletionProvider;
|
||||||
|
|
||||||
|
impl editor::CompletionProvider for SlashCommandCompletionProvider {
|
||||||
|
fn completions(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
buffer_position: language::Anchor,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
|
let Some((command_name, name_range)) = buffer.update(cx, |buffer, _cx| {
|
||||||
|
let position = buffer_position.to_point(buffer);
|
||||||
|
let line_start = Point::new(position.row, 0);
|
||||||
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||||
|
let line = lines.next()?;
|
||||||
|
let call = SlashCommandLine::parse(line)?;
|
||||||
|
|
||||||
|
if call.argument.is_some() {
|
||||||
|
// Don't autocomplete arguments.
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let name = line[call.name.clone()].to_string();
|
||||||
|
let name_range_start = Point::new(position.row, call.name.start as u32);
|
||||||
|
let name_range_end = Point::new(position.row, call.name.end as u32);
|
||||||
|
let name_range =
|
||||||
|
buffer.anchor_after(name_range_start)..buffer.anchor_after(name_range_end);
|
||||||
|
Some((name, name_range))
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
return Task::ready(Ok(Vec::new()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let commands = SlashCommandRegistry::global(cx);
|
||||||
|
let candidates = commands
|
||||||
|
.command_names()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, def)| StringMatchCandidate {
|
||||||
|
id: ix,
|
||||||
|
string: def.to_string(),
|
||||||
|
char_bag: def.as_ref().into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let command_name = command_name.to_string();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let matches = match_strings(
|
||||||
|
&candidates,
|
||||||
|
&command_name,
|
||||||
|
true,
|
||||||
|
usize::MAX,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background_executor().clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mat| {
|
||||||
|
let command = commands.command(&mat.string)?;
|
||||||
|
let mut new_text = mat.string.clone();
|
||||||
|
let requires_argument = command.requires_argument();
|
||||||
|
if requires_argument {
|
||||||
|
new_text.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(project::Completion {
|
||||||
|
old_range: name_range.clone(),
|
||||||
|
documentation: Some(Documentation::SingleLine(command.description())),
|
||||||
|
new_text,
|
||||||
|
label: command.label(cx),
|
||||||
|
server_id: LanguageServerId(0),
|
||||||
|
lsp_completion: Default::default(),
|
||||||
|
show_new_completions_on_confirm: false,
|
||||||
|
confirm: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
_: Model<Buffer>,
|
||||||
|
_: Vec<usize>,
|
||||||
|
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||||
|
_: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
Task::ready(Ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_additional_edits_for_completion(
|
||||||
|
&self,
|
||||||
|
_: Model<Buffer>,
|
||||||
|
_: project::Completion,
|
||||||
|
_: bool,
|
||||||
|
_: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Option<language::Transaction>>> {
|
||||||
|
Task::ready(Ok(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: language::Anchor,
|
||||||
|
_text: &str,
|
||||||
|
_trigger_in_words: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> bool {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let position = position.to_point(buffer);
|
||||||
|
let line_start = Point::new(position.row, 0);
|
||||||
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||||
|
if let Some(line) = lines.next() {
|
||||||
|
SlashCommandLine::parse(line).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -17,6 +17,7 @@ use std::{
|
|||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub mod active_command;
|
pub mod active_command;
|
||||||
|
pub mod default_command;
|
||||||
pub mod fetch_command;
|
pub mod fetch_command;
|
||||||
pub mod file_command;
|
pub mod file_command;
|
||||||
pub mod project_command;
|
pub mod project_command;
|
||||||
@ -117,6 +118,7 @@ impl SlashCommandCompletionProvider {
|
|||||||
command_range.clone(),
|
command_range.clone(),
|
||||||
&command_name,
|
&command_name,
|
||||||
None,
|
None,
|
||||||
|
true,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@ -178,6 +180,7 @@ impl SlashCommandCompletionProvider {
|
|||||||
command_range.clone(),
|
command_range.clone(),
|
||||||
&command_name,
|
&command_name,
|
||||||
Some(&arg),
|
Some(&arg),
|
||||||
|
true,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -96,6 +96,7 @@ impl SlashCommand for ActiveSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
81
crates/assistant/src/slash_command/default_command.rs
Normal file
81
crates/assistant/src/slash_command/default_command.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
|
||||||
|
use crate::prompt_library::PromptStore;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use assistant_slash_command::SlashCommandOutputSection;
|
||||||
|
use gpui::{AppContext, Task, WeakView};
|
||||||
|
use language::LspAdapterDelegate;
|
||||||
|
use std::{
|
||||||
|
fmt::Write,
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
use ui::prelude::*;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub(crate) struct DefaultSlashCommand;
|
||||||
|
|
||||||
|
impl SlashCommand for DefaultSlashCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"default".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"insert default prompt".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu_text(&self) -> String {
|
||||||
|
"Insert Default Prompt".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_argument(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_argument(
|
||||||
|
&self,
|
||||||
|
_query: String,
|
||||||
|
_cancellation_flag: Arc<AtomicBool>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
|
_cx: &mut AppContext,
|
||||||
|
) -> Task<Result<Vec<String>>> {
|
||||||
|
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
self: Arc<Self>,
|
||||||
|
_argument: Option<&str>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
|
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<SlashCommandOutput>> {
|
||||||
|
let store = PromptStore::global(cx);
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let store = store.await?;
|
||||||
|
let prompts = store.default_prompt_metadata();
|
||||||
|
|
||||||
|
let mut text = String::new();
|
||||||
|
writeln!(text, "Default Prompt:").unwrap();
|
||||||
|
for prompt in prompts {
|
||||||
|
if let Some(title) = prompt.title {
|
||||||
|
writeln!(text, "/prompt {}", title).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text.pop();
|
||||||
|
|
||||||
|
Ok(SlashCommandOutput {
|
||||||
|
sections: vec![SlashCommandOutputSection {
|
||||||
|
range: 0..text.len(),
|
||||||
|
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||||
|
PromptPlaceholder {
|
||||||
|
title: "Default".into(),
|
||||||
|
id,
|
||||||
|
unfold,
|
||||||
|
}
|
||||||
|
.into_any_element()
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
text,
|
||||||
|
run_commands_in_text: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -107,6 +107,7 @@ impl SlashCommand for FetchSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,7 @@ impl SlashCommand for FileSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -148,6 +148,7 @@ impl SlashCommand for ProjectSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -8,15 +8,7 @@ use std::sync::{atomic::AtomicBool, Arc};
|
|||||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct PromptSlashCommand {
|
pub(crate) struct PromptSlashCommand;
|
||||||
store: Arc<PromptStore>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PromptSlashCommand {
|
|
||||||
pub fn new(store: Arc<PromptStore>) -> Self {
|
|
||||||
Self { store }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlashCommand for PromptSlashCommand {
|
impl SlashCommand for PromptSlashCommand {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
@ -42,9 +34,9 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
_workspace: WeakView<Workspace>,
|
_workspace: WeakView<Workspace>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<Vec<String>>> {
|
) -> Task<Result<Vec<String>>> {
|
||||||
let store = self.store.clone();
|
let store = PromptStore::global(cx);
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
let prompts = store.search(query).await;
|
let prompts = store.await?.search(query).await;
|
||||||
Ok(prompts
|
Ok(prompts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||||
@ -63,11 +55,12 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||||
};
|
};
|
||||||
|
|
||||||
let store = self.store.clone();
|
let store = PromptStore::global(cx);
|
||||||
let title = SharedString::from(title.to_string());
|
let title = SharedString::from(title.to_string());
|
||||||
let prompt = cx.background_executor().spawn({
|
let prompt = cx.background_executor().spawn({
|
||||||
let title = title.clone();
|
let title = title.clone();
|
||||||
async move {
|
async move {
|
||||||
|
let store = store.await?;
|
||||||
let prompt_id = store
|
let prompt_id = store
|
||||||
.id_for_title(&title)
|
.id_for_title(&title)
|
||||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||||
@ -91,6 +84,7 @@ impl SlashCommand for PromptSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -192,6 +192,7 @@ impl SlashCommand for RustdocSlashCommand {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,11 @@ impl SlashCommand for SearchSlashCommand {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
SlashCommandOutput { text, sections }
|
SlashCommandOutput {
|
||||||
|
text,
|
||||||
|
sections,
|
||||||
|
run_commands_in_text: false,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -109,7 +109,11 @@ impl SlashCommand for TabsSlashCommand {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SlashCommandOutput { text, sections })
|
Ok(SlashCommandOutput {
|
||||||
|
text,
|
||||||
|
sections,
|
||||||
|
run_commands_in_text: false,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
Err(error) => Task::ready(Err(error)),
|
Err(error) => Task::ready(Err(error)),
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ pub type RenderFoldPlaceholder = Arc<
|
|||||||
pub struct SlashCommandOutput {
|
pub struct SlashCommandOutput {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||||
|
pub run_commands_in_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -100,6 +100,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
|
run_commands_in_text: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -656,7 +656,7 @@ impl MacWindow {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
|
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
|
||||||
{
|
{
|
||||||
native_window.setTitle_(NSString::alloc(nil).init_str(title));
|
window.set_title(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
native_window.setMovable_(is_movable as BOOL);
|
native_window.setMovable_(is_movable as BOOL);
|
||||||
|
@ -236,7 +236,12 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
/// If `scroll_to_index` is true, the new selected index will be scrolled into view.
|
/// If `scroll_to_index` is true, the new selected index will be scrolled into view.
|
||||||
///
|
///
|
||||||
/// If some effect is bound to `selected_index_changed`, it will be executed.
|
/// If some effect is bound to `selected_index_changed`, it will be executed.
|
||||||
fn set_selected_index(&mut self, ix: usize, scroll_to_index: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
scroll_to_index: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
let previous_index = self.delegate.selected_index();
|
let previous_index = self.delegate.selected_index();
|
||||||
self.delegate.set_selected_index(ix, cx);
|
self.delegate.set_selected_index(ix, cx);
|
||||||
let current_index = self.delegate.selected_index();
|
let current_index = self.delegate.selected_index();
|
||||||
|
@ -192,6 +192,7 @@ pub enum IconName {
|
|||||||
WholeWord,
|
WholeWord,
|
||||||
XCircle,
|
XCircle,
|
||||||
ZedAssistant,
|
ZedAssistant,
|
||||||
|
ZedAssistantFilled,
|
||||||
ZedXCopilot,
|
ZedXCopilot,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,6 +316,7 @@ impl IconName {
|
|||||||
IconName::WholeWord => "icons/word_search.svg",
|
IconName::WholeWord => "icons/word_search.svg",
|
||||||
IconName::XCircle => "icons/error.svg",
|
IconName::XCircle => "icons/error.svg",
|
||||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||||
|
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
|
||||||
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ pub struct ListSubHeader {
|
|||||||
label: SharedString,
|
label: SharedString,
|
||||||
start_slot: Option<IconName>,
|
start_slot: Option<IconName>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListSubHeader {
|
impl ListSubHeader {
|
||||||
@ -14,6 +15,7 @@ impl ListSubHeader {
|
|||||||
label: label.into(),
|
label: label.into(),
|
||||||
start_slot: None,
|
start_slot: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,12 +30,22 @@ impl ListSubHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Selectable for ListSubHeader {
|
||||||
|
fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for ListSubHeader {
|
impl RenderOnce for ListSubHeader {
|
||||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
h_flex().flex_1().w_full().relative().py_1().child(
|
h_flex().flex_1().w_full().relative().py_1().child(
|
||||||
div()
|
div()
|
||||||
.h_6()
|
.h_6()
|
||||||
.when(self.inset, |this| this.px_2())
|
.when(self.inset, |this| this.px_2())
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
Loading…
Reference in New Issue
Block a user