mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +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",
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"async-watch",
|
||||
"cargo_toml",
|
||||
"chrono",
|
||||
"client",
|
||||
@ -824,15 +823,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "async_zip"
|
||||
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
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
assistant_slash_command.workspace = true
|
||||
async-watch.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
|
@ -19,14 +19,13 @@ use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use model_selector::*;
|
||||
use prompt_library::PromptStore;
|
||||
pub(crate) use saved_conversation::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, fetch_command, file_command, project_command, prompt_command, rustdoc_command,
|
||||
search_command, tabs_command,
|
||||
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
|
||||
rustdoc_command, search_command, tabs_command,
|
||||
};
|
||||
use std::{
|
||||
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(project_command::ProjectSlashCommand, 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(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)]
|
||||
|
@ -1,11 +1,11 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
prompt_library::{open_prompt_library, PromptMetadata, PromptStore},
|
||||
prompt_library::open_prompt_library,
|
||||
prompts::generate_content_prompt,
|
||||
search::*,
|
||||
slash_command::{
|
||||
prompt_command::PromptPlaceholder, SlashCommandCompletionProvider, SlashCommandLine,
|
||||
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||
SlashCommandRegistry,
|
||||
},
|
||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
||||
@ -14,7 +14,7 @@ use crate::{
|
||||
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleModelSelector,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use editor::{actions::ShowCompletions, GutterDimensions};
|
||||
@ -40,10 +40,9 @@ use gpui::{
|
||||
Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use language::LspAdapterDelegate;
|
||||
use language::{
|
||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||
OffsetRangeExt as _, Point, ToOffset as _,
|
||||
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@ -118,14 +117,6 @@ pub struct AssistantPanel {
|
||||
_watch_saved_conversations: Task<Result<()>>,
|
||||
authentication_prompt: Option<AnyView>,
|
||||
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
default_prompt: DefaultPrompt,
|
||||
_watch_prompt_store: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DefaultPrompt {
|
||||
text: String,
|
||||
sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
struct ActiveConversationEditor {
|
||||
@ -146,8 +137,6 @@ impl AssistantPanel {
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
|
||||
let default_prompts = prompt_store.load_default().await?;
|
||||
|
||||
// TODO: deserialize state.
|
||||
let workspace_handle = workspace.clone();
|
||||
@ -173,22 +162,6 @@ impl AssistantPanel {
|
||||
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 mut toolbar = Toolbar::new();
|
||||
toolbar.set_can_navigate(false, cx);
|
||||
@ -216,7 +189,7 @@ impl AssistantPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
Self {
|
||||
workspace: workspace_handle,
|
||||
active_conversation_editor: None,
|
||||
show_saved_conversations: false,
|
||||
@ -239,11 +212,7 @@ impl AssistantPanel {
|
||||
_watch_saved_conversations,
|
||||
authentication_prompt: None,
|
||||
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();
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
prev_settings_version: usize,
|
||||
@ -862,7 +782,6 @@ impl AssistantPanel {
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
ConversationEditor::new(
|
||||
&self.default_prompt,
|
||||
self.languages.clone(),
|
||||
self.slash_commands.clone(),
|
||||
self.fs.clone(),
|
||||
@ -1460,7 +1379,9 @@ enum ConversationEvent {
|
||||
updated: Vec<PendingSlashCommand>,
|
||||
},
|
||||
SlashCommandFinished {
|
||||
output_range: Range<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),
|
||||
));
|
||||
|
||||
let start_ix = match self
|
||||
.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 old_range = self.pending_command_indices_for_range(start..end, cx);
|
||||
|
||||
let mut new_commands = Vec::new();
|
||||
let mut lines = buffer.text_for_range(start..end).lines();
|
||||
@ -1773,9 +1683,7 @@ impl Conversation {
|
||||
offset = lines.offset();
|
||||
}
|
||||
|
||||
let removed_commands = self
|
||||
.pending_slash_commands
|
||||
.splice(start_ix..end_ix, new_commands);
|
||||
let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
|
||||
removed.extend(removed_commands.map(|command| command.source_range));
|
||||
}
|
||||
|
||||
@ -1849,25 +1757,60 @@ impl Conversation {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&mut PendingSlashCommand> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let ix = self
|
||||
match self
|
||||
.pending_slash_commands
|
||||
.binary_search_by(|probe| {
|
||||
if probe.source_range.start.cmp(&position, buffer).is_gt() {
|
||||
Ordering::Less
|
||||
} else if probe.source_range.end.cmp(&position, buffer).is_lt() {
|
||||
Ordering::Greater
|
||||
.binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
|
||||
{
|
||||
Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
|
||||
Err(ix) => {
|
||||
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 {
|
||||
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(
|
||||
&mut self,
|
||||
command_range: Range<language::Anchor>,
|
||||
output: Task<Result<SlashCommandOutput>>,
|
||||
insert_trailing_newline: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.reparse_slash_commands(cx);
|
||||
@ -1878,13 +1821,14 @@ impl Conversation {
|
||||
let output = output.await;
|
||||
this.update(&mut cx, |this, cx| match output {
|
||||
Ok(mut output) => {
|
||||
if !output.text.ends_with('\n') {
|
||||
if insert_trailing_newline {
|
||||
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 old_end = command_range.end.to_offset(buffer);
|
||||
let new_end = start + output.text.len();
|
||||
buffer.edit([(start..old_end, output.text)], None, cx);
|
||||
|
||||
let mut sections = output
|
||||
@ -1897,9 +1841,14 @@ impl Conversation {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
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) => {
|
||||
if let Some(pending_command) =
|
||||
@ -2596,7 +2545,6 @@ pub struct ConversationEditor {
|
||||
|
||||
impl ConversationEditor {
|
||||
fn new(
|
||||
default_prompt: &DefaultPrompt,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
slash_command_registry: Arc<SlashCommandRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@ -2618,31 +2566,7 @@ impl ConversationEditor {
|
||||
|
||||
let mut this =
|
||||
Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, 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.insert_default_prompt(cx);
|
||||
this
|
||||
}
|
||||
|
||||
@ -2695,6 +2619,32 @@ impl ConversationEditor {
|
||||
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>) {
|
||||
let cursors = self.cursors(cx);
|
||||
|
||||
@ -2817,6 +2767,7 @@ impl ConversationEditor {
|
||||
command.source_range,
|
||||
&command.name,
|
||||
command.argument.as_deref(),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
@ -2830,6 +2781,7 @@ impl ConversationEditor {
|
||||
command_range: Range<language::Anchor>,
|
||||
name: &str,
|
||||
argument: Option<&str>,
|
||||
insert_trailing_newline: bool,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@ -2838,7 +2790,12 @@ impl ConversationEditor {
|
||||
let argument = argument.map(ToString::to_string);
|
||||
let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, 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.name,
|
||||
command.argument.as_deref(),
|
||||
false,
|
||||
workspace.clone(),
|
||||
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);
|
||||
|
||||
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 assistant_slash_command::SlashCommandRegistry;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use editor::{actions::Tab, Editor, EditorEvent};
|
||||
use futures::{
|
||||
future::{self, BoxFuture, Shared},
|
||||
FutureExt,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, point, size, AppContext, BackgroundExecutor, Bounds, DevicePixels, Empty,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
|
||||
WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, point, size, AnyElement, AppContext, BackgroundExecutor, Bounds, DevicePixels,
|
||||
EventEmitter, Global, Model, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
|
||||
View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
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 picker::{Picker, PickerDelegate};
|
||||
use rope::Rope;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
|
||||
ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
|
||||
};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@ -80,7 +87,7 @@ pub fn open_prompt_library(
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: None,
|
||||
title: Some("Prompt Library".into()),
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
}),
|
||||
@ -106,6 +113,8 @@ pub struct PromptLibrary {
|
||||
|
||||
struct PromptEditor {
|
||||
editor: View<Editor>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
next_body_to_save: Option<Rope>,
|
||||
pending_save: Option<Task<Option<()>>>,
|
||||
_subscription: Subscription,
|
||||
@ -114,30 +123,54 @@ struct PromptEditor {
|
||||
struct PromptPickerDelegate {
|
||||
store: Arc<PromptStore>,
|
||||
selected_index: usize,
|
||||
matches: Vec<PromptMetadata>,
|
||||
entries: Vec<PromptPickerEntry>,
|
||||
}
|
||||
|
||||
enum PromptPickerEvent {
|
||||
Selected { prompt_id: PromptId },
|
||||
Confirmed { prompt_id: PromptId },
|
||||
Deleted { 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 PickerDelegate for PromptPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
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;
|
||||
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> {
|
||||
@ -146,11 +179,49 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
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 {
|
||||
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.delegate.selected_index = 0;
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.entries = entries;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
@ -158,7 +229,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
}
|
||||
|
||||
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 {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
@ -173,61 +244,80 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
let prompt = self.entries.get(ix)?;
|
||||
let element = match prompt {
|
||||
PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::ZedAssistant))
|
||||
.selected(selected)
|
||||
.child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
))
|
||||
.end_slot(if default {
|
||||
IconButton::new("toggle-default-prompt", IconName::StarFilled)
|
||||
.shape(IconButtonShape::Square)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Empty.into_any()
|
||||
})
|
||||
.end_hover_slot(
|
||||
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 })
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if default {
|
||||
IconName::StarFilled
|
||||
} else {
|
||||
IconName::Star
|
||||
},
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::DefaultPromptsEmpty => {
|
||||
ListSubHeader::new("Star a prompt to add it to the default context")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element()
|
||||
}
|
||||
PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
|
||||
.inset(true)
|
||||
.start_slot(Icon::new(IconName::Library))
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.into_any_element(),
|
||||
PromptPickerEntry::Prompt(prompt) => {
|
||||
let default = prompt.default;
|
||||
let prompt_id = prompt.id;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
))
|
||||
.end_hover_slot(
|
||||
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)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(
|
||||
.child(
|
||||
IconButton::new(
|
||||
"toggle-default-prompt",
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
IconName::ZedAssistantFilled
|
||||
} else {
|
||||
"Add to Default Prompt"
|
||||
IconName::ZedAssistant
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
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 {
|
||||
store: store.clone(),
|
||||
selected_index: 0,
|
||||
matches: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
let picker = Picker::list(delegate, cx).modal(false).max_height(None);
|
||||
picker.focus(cx);
|
||||
picker
|
||||
});
|
||||
let mut this = Self {
|
||||
Self {
|
||||
store: store.clone(),
|
||||
language_registry,
|
||||
prompt_editors: HashMap::default(),
|
||||
@ -258,11 +346,7 @@ impl PromptLibrary {
|
||||
pending_load: Task::ready(()),
|
||||
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
|
||||
picker,
|
||||
};
|
||||
if let Some(prompt_id) = store.most_recently_saved() {
|
||||
this.load_prompt(prompt_id, false, cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
fn handle_picker_event(
|
||||
@ -272,6 +356,9 @@ impl PromptLibrary {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
}
|
||||
@ -285,6 +372,15 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
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 save = self.store.save(prompt_id, None, false, "".into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
@ -383,7 +479,7 @@ impl PromptLibrary {
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
}
|
||||
self.active_prompt_id = Some(prompt_id);
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
} else {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
@ -404,6 +500,8 @@ impl PromptLibrary {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor
|
||||
.set_completion_provider(Box::new(SlashCommandCompletionProvider));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
}
|
||||
@ -419,11 +517,13 @@ impl PromptLibrary {
|
||||
editor,
|
||||
next_body_to_save: None,
|
||||
pending_save: None,
|
||||
token_count: None,
|
||||
pending_token_count: Task::ready(None),
|
||||
_subscription,
|
||||
},
|
||||
);
|
||||
this.active_prompt_id = Some(prompt_id);
|
||||
cx.notify();
|
||||
this.set_active_prompt(Some(prompt_id), cx);
|
||||
this.count_tokens(prompt_id, cx);
|
||||
}
|
||||
Err(error) => {
|
||||
// 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>) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
let confirmation = cx.prompt(
|
||||
@ -451,7 +577,7 @@ impl PromptLibrary {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
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.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(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
@ -502,12 +641,53 @@ impl PromptLibrary {
|
||||
});
|
||||
|
||||
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 {
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.h_full()
|
||||
.w_1_3()
|
||||
@ -545,64 +725,69 @@ impl PromptLibrary {
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|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(
|
||||
v_flex()
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(TitleBar::height(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.justify_end()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.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));
|
||||
}),
|
||||
),
|
||||
),
|
||||
div()
|
||||
.on_action(cx.listener(Self::focus_picker))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.pt(Spacing::Large.rems(cx))
|
||||
.pl(Spacing::Large.rems(cx))
|
||||
.child(prompt_editor.editor.clone()),
|
||||
)
|
||||
.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>>,
|
||||
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
|
||||
metadata_cache: RwLock<MetadataCache>,
|
||||
updates: (Arc<async_watch::Sender<()>>, async_watch::Receiver<()>),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -668,9 +852,7 @@ impl MetadataCache {
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||
}
|
||||
cache
|
||||
.metadata
|
||||
.sort_unstable_by_key(|metadata| Reverse(metadata.saved_at));
|
||||
cache.sort();
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
@ -681,13 +863,21 @@ impl MetadataCache {
|
||||
} else {
|
||||
self.metadata.push(metadata);
|
||||
}
|
||||
self.metadata.sort_by_key(|m| Reverse(m.saved_at));
|
||||
self.sort();
|
||||
}
|
||||
|
||||
fn remove(&mut self, id: PromptId) {
|
||||
self.metadata.retain(|metadata| metadata.id != 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 {
|
||||
@ -715,23 +905,17 @@ impl PromptStore {
|
||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
let (updates_tx, updates_rx) = async_watch::channel(());
|
||||
Ok(PromptStore {
|
||||
executor,
|
||||
env: db_env,
|
||||
bodies,
|
||||
metadata,
|
||||
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>> {
|
||||
let env = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
@ -743,8 +927,8 @@ impl PromptStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_default(&self) -> Task<Result<Vec<(PromptMetadata, String)>>> {
|
||||
let default_metadatas = self
|
||||
pub fn default_prompt_metadata(&self) -> Vec<PromptMetadata> {
|
||||
return self
|
||||
.metadata_cache
|
||||
.read()
|
||||
.metadata
|
||||
@ -752,23 +936,6 @@ impl PromptStore {
|
||||
.filter(|metadata| metadata.default)
|
||||
.cloned()
|
||||
.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<()>> {
|
||||
@ -854,7 +1021,6 @@ impl PromptStore {
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
let updates = self.updates.0.clone();
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
@ -863,7 +1029,6 @@ impl PromptStore {
|
||||
bodies.put(&mut txn, &id, &body.to_string())?;
|
||||
|
||||
txn.commit()?;
|
||||
updates.send(()).ok();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@ -885,24 +1050,18 @@ impl PromptStore {
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let metadata = self.metadata;
|
||||
let updates = self.updates.0.clone();
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
txn.commit()?;
|
||||
updates.send(()).ok();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn most_recently_saved(&self) -> Option<PromptId> {
|
||||
self.metadata_cache
|
||||
.read()
|
||||
.metadata
|
||||
.first()
|
||||
.map(|metadata| metadata.id)
|
||||
fn first(&self) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata.first().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
@ -933,3 +1092,123 @@ fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString>
|
||||
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;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod default_command;
|
||||
pub mod fetch_command;
|
||||
pub mod file_command;
|
||||
pub mod project_command;
|
||||
@ -117,6 +118,7 @@ impl SlashCommandCompletionProvider {
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
@ -178,6 +180,7 @@ impl SlashCommandCompletionProvider {
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
@ -96,6 +96,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
.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()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -187,6 +187,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
@ -8,15 +8,7 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
store: Arc<PromptStore>,
|
||||
}
|
||||
|
||||
impl PromptSlashCommand {
|
||||
pub fn new(store: Arc<PromptStore>) -> Self {
|
||||
Self { store }
|
||||
}
|
||||
}
|
||||
pub(crate) struct PromptSlashCommand;
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
@ -42,9 +34,9 @@ impl SlashCommand for PromptSlashCommand {
|
||||
_workspace: WeakView<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let store = self.store.clone();
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let prompts = store.search(query).await;
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||
@ -63,11 +55,12 @@ impl SlashCommand for PromptSlashCommand {
|
||||
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 prompt = cx.background_executor().spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
@ -91,6 +84,7 @@ impl SlashCommand for PromptSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -192,6 +192,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
.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;
|
||||
|
||||
|
@ -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)),
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -100,6 +100,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
}
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -656,7 +656,7 @@ impl MacWindow {
|
||||
.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);
|
||||
|
@ -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 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();
|
||||
self.delegate.set_selected_index(ix, cx);
|
||||
let current_index = self.delegate.selected_index();
|
||||
|
@ -192,6 +192,7 @@ pub enum IconName {
|
||||
WholeWord,
|
||||
XCircle,
|
||||
ZedAssistant,
|
||||
ZedAssistantFilled,
|
||||
ZedXCopilot,
|
||||
}
|
||||
|
||||
@ -315,6 +316,7 @@ impl IconName {
|
||||
IconName::WholeWord => "icons/word_search.svg",
|
||||
IconName::XCircle => "icons/error.svg",
|
||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
|
||||
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
start_slot: Option<IconName>,
|
||||
inset: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ListSubHeader {
|
||||
@ -14,6 +15,7 @@ impl ListSubHeader {
|
||||
label: label.into(),
|
||||
start_slot: None,
|
||||
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 {
|
||||
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(
|
||||
div()
|
||||
.h_6()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.flex()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
|
Loading…
Reference in New Issue
Block a user