mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-26 20:22:30 +00:00
Rework context insertion UX (#12360)
- Confirming a completion now runs the command immediately - Hitting `enter` on a line with a command now runs it - The output of commands gets folded away and replaced with a custom placeholder - Eliminated ambient context <img width="1588" alt="image" src="https://github.com/zed-industries/zed/assets/482957/b1927a45-52d6-4634-acc9-2ee539c1d89a"> Release Notes: - N/A --------- Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
20f37f0647
commit
7e3ab9acc9
32 changed files with 1148 additions and 1534 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -434,10 +434,10 @@ dependencies = [
|
|||
"anyhow",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"parking_lot",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3823,6 +3823,7 @@ dependencies = [
|
|||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
"wit-component",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
1
assets/icons/triangle_right.svg
Normal file
1
assets/icons/triangle_right.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 11L6 4L10.5 7.5L6 11Z" fill="currentColor"></path></svg>
|
After Width: | Height: | Size: 164 B |
|
@ -211,7 +211,9 @@
|
|||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole"
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -227,7 +227,9 @@
|
|||
"cmd-s": "workspace::Save",
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole"
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
mod current_project;
|
||||
mod recent_buffers;
|
||||
|
||||
pub use current_project::*;
|
||||
pub use recent_buffers::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AmbientContext {
|
||||
pub recent_buffers: RecentBuffersContext,
|
||||
pub current_project: CurrentProjectContext,
|
||||
}
|
||||
|
||||
impl AmbientContext {
|
||||
pub fn snapshot(&self) -> AmbientContextSnapshot {
|
||||
AmbientContextSnapshot {
|
||||
recent_buffers: self.recent_buffers.snapshot.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct AmbientContextSnapshot {
|
||||
pub recent_buffers: RecentBuffersSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||
pub enum ContextUpdated {
|
||||
Updating,
|
||||
Disabled,
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
|
||||
use project::{Project, ProjectPath};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::ambient_context::ContextUpdated;
|
||||
use crate::assistant_panel::Conversation;
|
||||
use crate::{LanguageModelRequestMessage, Role};
|
||||
|
||||
/// Ambient context about the current project.
|
||||
pub struct CurrentProjectContext {
|
||||
pub enabled: bool,
|
||||
pub message: String,
|
||||
pub pending_message: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for CurrentProjectContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
message: String::new(),
|
||||
pending_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentProjectContext {
|
||||
/// Returns the [`CurrentProjectContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled
|
||||
.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.message.clone(),
|
||||
})
|
||||
.filter(|message| !message.content.is_empty())
|
||||
}
|
||||
|
||||
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
|
||||
pub fn update(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
project: WeakModel<Project>,
|
||||
cx: &mut ModelContext<Conversation>,
|
||||
) -> ContextUpdated {
|
||||
if !self.enabled {
|
||||
self.message.clear();
|
||||
self.pending_message = None;
|
||||
cx.notify();
|
||||
return ContextUpdated::Disabled;
|
||||
}
|
||||
|
||||
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
|
||||
let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(path_to_cargo_toml) = path_to_cargo_toml
|
||||
.ok_or_else(|| anyhow!("no Cargo.toml"))
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let message_task = cx
|
||||
.background_executor()
|
||||
.spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
|
||||
|
||||
if let Some(message) = message_task.await.log_err() {
|
||||
conversation
|
||||
.update(&mut cx, |conversation, cx| {
|
||||
conversation.ambient_context.current_project.message = message;
|
||||
conversation.count_remaining_tokens(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
|
||||
ContextUpdated::Updating
|
||||
}
|
||||
|
||||
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
|
||||
let buffer = fs.load(path_to_cargo_toml).await?;
|
||||
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
|
||||
|
||||
let mut message = String::new();
|
||||
writeln!(message, "You are in a Rust project.")?;
|
||||
|
||||
if let Some(workspace) = cargo_toml.workspace {
|
||||
writeln!(
|
||||
message,
|
||||
"The project is a Cargo workspace with the following members:"
|
||||
)?;
|
||||
for member in workspace.members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
|
||||
if !workspace.default_members.is_empty() {
|
||||
writeln!(message, "The default members are:")?;
|
||||
for member in workspace.default_members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if !workspace.dependencies.is_empty() {
|
||||
writeln!(
|
||||
message,
|
||||
"The following workspace dependencies are installed:"
|
||||
)?;
|
||||
for dependency in workspace.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
} else if let Some(package) = cargo_toml.package {
|
||||
writeln!(
|
||||
message,
|
||||
"The project name is \"{name}\".",
|
||||
name = package.name
|
||||
)?;
|
||||
|
||||
let description = package
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|description| description.get().ok().cloned());
|
||||
if let Some(description) = description.as_ref() {
|
||||
writeln!(message, "It describes itself as \"{description}\".")?;
|
||||
}
|
||||
|
||||
if !cargo_toml.dependencies.is_empty() {
|
||||
writeln!(message, "The following dependencies are installed:")?;
|
||||
for dependency in cargo_toml.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(
|
||||
project: WeakModel<Project>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
cx.update(|cx| {
|
||||
let worktree = project.update(cx, |project, _cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no worktree"))
|
||||
})??;
|
||||
|
||||
let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
|
||||
let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: cargo_toml.path.clone(),
|
||||
})
|
||||
});
|
||||
let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
|
||||
project
|
||||
.update(cx, |project, cx| project.absolute_path(&path, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
Ok(path_to_cargo_toml)
|
||||
})?
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
|
||||
use gpui::{ModelContext, Subscription, Task, WeakModel};
|
||||
use language::{Buffer, BufferSnapshot, Rope};
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
|
||||
use super::ContextUpdated;
|
||||
|
||||
pub struct RecentBuffersContext {
|
||||
pub enabled: bool,
|
||||
pub buffers: Vec<RecentBuffer>,
|
||||
pub snapshot: RecentBuffersSnapshot,
|
||||
pub pending_message: Option<Task<()>>,
|
||||
}
|
||||
|
||||
pub struct RecentBuffer {
|
||||
pub buffer: WeakModel<Buffer>,
|
||||
pub _subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Default for RecentBuffersContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
buffers: Vec::new(),
|
||||
snapshot: RecentBuffersSnapshot::default(),
|
||||
pending_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RecentBuffersContext {
|
||||
pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
|
||||
let source_buffers = self
|
||||
.buffers
|
||||
.iter()
|
||||
.filter_map(|recent| {
|
||||
let (full_path, snapshot) = recent
|
||||
.buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
(
|
||||
buffer.file().map(|file| file.full_path(cx)),
|
||||
buffer.snapshot(),
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
Some(SourceBufferSnapshot {
|
||||
full_path,
|
||||
model: recent.buffer.clone(),
|
||||
snapshot,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !self.enabled || source_buffers.is_empty() {
|
||||
self.snapshot.message = Default::default();
|
||||
self.snapshot.source_buffers.clear();
|
||||
self.pending_message = None;
|
||||
cx.notify();
|
||||
ContextUpdated::Disabled
|
||||
} else {
|
||||
self.pending_message = Some(cx.spawn(|this, mut cx| async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
|
||||
let message = if source_buffers.is_empty() {
|
||||
Rope::new()
|
||||
} else {
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let source_buffers = source_buffers.clone();
|
||||
async move { message_for_recent_buffers(source_buffers) }
|
||||
})
|
||||
.await
|
||||
};
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
|
||||
this.ambient_context.recent_buffers.snapshot.message = message;
|
||||
this.count_remaining_tokens(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
|
||||
ContextUpdated::Updating
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`RecentBuffersContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled
|
||||
.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.snapshot.message.to_string(),
|
||||
})
|
||||
.filter(|message| !message.content.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct RecentBuffersSnapshot {
|
||||
pub message: Rope,
|
||||
pub source_buffers: Vec<SourceBufferSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SourceBufferSnapshot {
|
||||
pub full_path: Option<PathBuf>,
|
||||
pub model: WeakModel<Buffer>,
|
||||
pub snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SourceBufferSnapshot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SourceBufferSnapshot")
|
||||
.field("full_path", &self.full_path)
|
||||
.field("model (entity id)", &self.model.entity_id())
|
||||
.field("snapshot (text)", &self.snapshot.text())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
|
||||
let mut message = String::new();
|
||||
writeln!(
|
||||
message,
|
||||
"The following is a list of recent buffers that the user has opened."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for buffer in buffers {
|
||||
if let Some(path) = buffer.full_path {
|
||||
writeln!(message, "```{}", path.display()).unwrap();
|
||||
} else {
|
||||
writeln!(message, "```untitled").unwrap();
|
||||
}
|
||||
|
||||
for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
|
||||
message.push_str(chunk.text);
|
||||
}
|
||||
if !message.ends_with('\n') {
|
||||
message.push('\n');
|
||||
}
|
||||
message.push_str("```\n");
|
||||
}
|
||||
|
||||
Rope::from(message.as_str())
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
mod ambient_context;
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod omit_ranges;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
use ambient_context::AmbientContextSnapshot;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
|
@ -38,7 +36,8 @@ actions!(
|
|||
InsertActivePrompt,
|
||||
ToggleIncludeConversation,
|
||||
ToggleHistory,
|
||||
ApplyEdit
|
||||
ApplyEdit,
|
||||
ConfirmCommand
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -188,9 +187,6 @@ pub struct LanguageModelChoiceDelta {
|
|||
struct MessageMetadata {
|
||||
role: Role,
|
||||
status: MessageStatus,
|
||||
// TODO: Delete this
|
||||
#[serde(skip)]
|
||||
ambient_context: AmbientContextSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,101 +0,0 @@
|
|||
use rope::Rope;
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
|
||||
pub(crate) fn text_in_range_omitting_ranges(
|
||||
rope: &Rope,
|
||||
range: Range<usize>,
|
||||
omit_ranges: &[Range<usize>],
|
||||
) -> String {
|
||||
let mut content = String::with_capacity(range.len());
|
||||
let mut omit_ranges = omit_ranges
|
||||
.iter()
|
||||
.skip_while(|omit_range| omit_range.end <= range.start)
|
||||
.peekable();
|
||||
let mut offset = range.start;
|
||||
let mut chunks = rope.chunks_in_range(range.clone());
|
||||
while let Some(chunk) = chunks.next() {
|
||||
if let Some(omit_range) = omit_ranges.peek() {
|
||||
match offset.cmp(&omit_range.start) {
|
||||
Ordering::Less => {
|
||||
let max_len = omit_range.start - offset;
|
||||
if chunk.len() < max_len {
|
||||
content.push_str(chunk);
|
||||
offset += chunk.len();
|
||||
} else {
|
||||
content.push_str(&chunk[..max_len]);
|
||||
chunks.seek(omit_range.end.min(range.end));
|
||||
offset = omit_range.end;
|
||||
omit_ranges.next();
|
||||
}
|
||||
}
|
||||
Ordering::Equal | Ordering::Greater => {
|
||||
chunks.seek(omit_range.end.min(range.end));
|
||||
offset = omit_range.end;
|
||||
omit_ranges.next();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content.push_str(chunk);
|
||||
offset += chunk.len();
|
||||
}
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{rngs::StdRng, Rng as _};
|
||||
use util::RandomCharIter;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_text_in_range_omitting_ranges(mut rng: StdRng) {
|
||||
let text = RandomCharIter::new(&mut rng).take(1024).collect::<String>();
|
||||
let rope = Rope::from(text.as_str());
|
||||
|
||||
let mut start = rng.gen_range(0..=text.len() / 2);
|
||||
let mut end = rng.gen_range(text.len() / 2..=text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start -= 1;
|
||||
}
|
||||
while !text.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
let range = start..end;
|
||||
|
||||
let mut ix = 0;
|
||||
let mut omit_ranges = Vec::new();
|
||||
for _ in 0..rng.gen_range(0..10) {
|
||||
let mut start = rng.gen_range(ix..=text.len());
|
||||
while !text.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
let mut end = rng.gen_range(start..=text.len());
|
||||
while !text.is_char_boundary(end) {
|
||||
end += 1;
|
||||
}
|
||||
omit_ranges.push(start..end);
|
||||
ix = end;
|
||||
if ix == text.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut expected_text = text[range.clone()].to_string();
|
||||
for omit_range in omit_ranges.iter().rev() {
|
||||
let start = omit_range
|
||||
.start
|
||||
.saturating_sub(range.start)
|
||||
.min(range.len());
|
||||
let end = omit_range.end.saturating_sub(range.start).min(range.len());
|
||||
expected_text.replace_range(start..end, "");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
text_in_range_omitting_ranges(&rope, range.clone(), &omit_ranges),
|
||||
expected_text,
|
||||
"text: {text:?}\nrange: {range:?}\nomit_ranges: {omit_ranges:?}"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
use crate::assistant_panel::ConversationEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext};
|
||||
use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use rope::Point;
|
||||
|
@ -12,18 +14,18 @@ use std::{
|
|||
Arc,
|
||||
},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use assistant_slash_command::{
|
||||
SlashCommand, SlashCommandCleanup, SlashCommandInvocation, SlashCommandRegistry,
|
||||
};
|
||||
|
||||
pub mod current_file_command;
|
||||
pub mod active_command;
|
||||
pub mod file_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
|
@ -34,18 +36,25 @@ pub(crate) struct SlashCommandLine {
|
|||
}
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
|
||||
pub fn new(
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
editor,
|
||||
commands,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_command_name(
|
||||
&self,
|
||||
command_name: &str,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut AppContext,
|
||||
command_range: Range<Anchor>,
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let candidates = self
|
||||
.commands
|
||||
|
@ -60,6 +69,8 @@ impl SlashCommandCompletionProvider {
|
|||
.collect::<Vec<_>>();
|
||||
let commands = self.commands.clone();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
executor.clone().spawn(async move {
|
||||
let matches = match_strings(
|
||||
|
@ -77,17 +88,37 @@ impl SlashCommandCompletionProvider {
|
|||
.filter_map(|mat| {
|
||||
let command = commands.command(&mat.string)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
if command.requires_argument() {
|
||||
let requires_argument = command.requires_argument();
|
||||
if requires_argument {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
Some(project::Completion {
|
||||
old_range: range.clone(),
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
new_text,
|
||||
label: CodeLabel::plain(mat.string, None),
|
||||
label: CodeLabel::plain(mat.string.clone(), None),
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm: (!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
}),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
|
@ -98,8 +129,9 @@ impl SlashCommandCompletionProvider {
|
|||
&self,
|
||||
command_name: &str,
|
||||
argument: String,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut AppContext,
|
||||
command_range: Range<Anchor>,
|
||||
argument_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
|
@ -108,17 +140,39 @@ impl SlashCommandCompletionProvider {
|
|||
|
||||
if let Some(command) = self.commands.command(command_name) {
|
||||
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
|
||||
let command_name: Arc<str> = command_name.into();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|arg| project::Completion {
|
||||
old_range: range.clone(),
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm: Some(Arc::new({
|
||||
let command_name = command_name.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
@ -136,25 +190,44 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||
buffer_position: Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let task = 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)?;
|
||||
let Some((name, argument, command_range, argument_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)?;
|
||||
|
||||
let name = &line[call.name.clone()];
|
||||
if let Some(argument) = call.argument {
|
||||
let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
||||
let argument = line[argument.clone()].to_string();
|
||||
Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
|
||||
} else {
|
||||
let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
||||
Some(self.complete_command_name(name, start..buffer_position, cx))
|
||||
}
|
||||
});
|
||||
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
|
||||
let command_range_end = Point::new(
|
||||
position.row,
|
||||
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
|
||||
);
|
||||
let command_range = buffer.anchor_after(command_range_start)
|
||||
..buffer.anchor_after(command_range_end);
|
||||
|
||||
task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
|
||||
let name = line[call.name.clone()].to_string();
|
||||
|
||||
Some(if let Some(argument) = call.argument {
|
||||
let start =
|
||||
buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
||||
let argument = line[argument.clone()].to_string();
|
||||
(name, Some(argument), command_range, start..buffer_position)
|
||||
} else {
|
||||
let start =
|
||||
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
||||
(name, None, command_range, start..buffer_position)
|
||||
})
|
||||
})
|
||||
else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
if let Some(argument) = argument {
|
||||
self.complete_command_argument(&name, argument, command_range, argument_range, cx)
|
||||
} else {
|
||||
self.complete_command_name(&name, command_range, argument_range, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
|
|
117
crates/assistant/src/slash_command/active_command.rs
Normal file
117
crates/assistant/src/slash_command/active_command.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Entity, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use ui::{IntoElement, WindowContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ActiveSlashCommand;
|
||||
|
||||
impl SlashCommand for ActiveSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"active".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert active tab".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert active tab".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let mut timestamps_by_entity_id = HashMap::default();
|
||||
for pane in workspace.panes() {
|
||||
let pane = pane.read(cx);
|
||||
for entry in pane.activation_history() {
|
||||
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
let mut most_recent_buffer = None;
|
||||
for editor in workspace.items_of_type::<Editor>(cx) {
|
||||
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let timestamp = timestamps_by_entity_id
|
||||
.get(&editor.entity_id())
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
if most_recent_buffer
|
||||
.as_ref()
|
||||
.map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
|
||||
{
|
||||
most_recent_buffer = Some((buffer, timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((buffer, _)) = most_recent_buffer {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
let text = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::Borrowed("untitled"));
|
||||
|
||||
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&path);
|
||||
output.push('\n');
|
||||
for chunk in snapshot.as_rope().chunks() {
|
||||
output.push_str(chunk);
|
||||
}
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
output
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
Ok(SlashCommandOutput {
|
||||
text: text.await,
|
||||
render_placeholder: Arc::new(move |id, unfold, _| {
|
||||
FilePlaceholder {
|
||||
id,
|
||||
path: path.clone(),
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no recent buffer found")))
|
||||
}
|
||||
});
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, cell::Cell, rc::Rc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
|
||||
use language::LspAdapterDelegate;
|
||||
use workspace::{Event as WorkspaceEvent, Workspace};
|
||||
|
||||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
|
||||
pub(crate) struct CurrentFileSlashCommand {
|
||||
workspace: WindowHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl CurrentFileSlashCommand {
|
||||
pub fn new(workspace: WindowHandle<Workspace>) -> Self {
|
||||
Self { workspace }
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for CurrentFileSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"current_file".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert the current file".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
let (invalidate_tx, invalidate_rx) = oneshot::channel();
|
||||
let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
|
||||
let mut subscriptions: Vec<Subscription> = Vec::new();
|
||||
let output = self.workspace.update(cx, |workspace, cx| {
|
||||
let mut timestamps_by_entity_id = HashMap::default();
|
||||
for pane in workspace.panes() {
|
||||
let pane = pane.read(cx);
|
||||
for entry in pane.activation_history() {
|
||||
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
let mut most_recent_buffer = None;
|
||||
for editor in workspace.items_of_type::<Editor>(cx) {
|
||||
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let timestamp = timestamps_by_entity_id
|
||||
.get(&editor.entity_id())
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
if most_recent_buffer
|
||||
.as_ref()
|
||||
.map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
|
||||
{
|
||||
most_recent_buffer = Some((buffer, timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions.push({
|
||||
let workspace_view = cx.view().clone();
|
||||
let invalidate_tx = invalidate_tx.clone();
|
||||
cx.window_context()
|
||||
.subscribe(&workspace_view, move |_workspace, event, _cx| match event {
|
||||
WorkspaceEvent::ActiveItemChanged
|
||||
| WorkspaceEvent::ItemAdded
|
||||
| WorkspaceEvent::ItemRemoved
|
||||
| WorkspaceEvent::PaneAdded(_)
|
||||
| WorkspaceEvent::PaneRemoved => {
|
||||
if let Some(invalidate_tx) = invalidate_tx.take() {
|
||||
_ = invalidate_tx.send(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((buffer, _)) = most_recent_buffer {
|
||||
subscriptions.push({
|
||||
let invalidate_tx = invalidate_tx.clone();
|
||||
cx.window_context().observe(&buffer, move |_buffer, _cx| {
|
||||
if let Some(invalidate_tx) = invalidate_tx.take() {
|
||||
_ = invalidate_tx.send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let path = snapshot.resolve_file_path(cx, true);
|
||||
cx.background_executor().spawn(async move {
|
||||
let path = path
|
||||
.as_ref()
|
||||
.map(|path| path.to_string_lossy())
|
||||
.unwrap_or_else(|| Cow::Borrowed("untitled"));
|
||||
|
||||
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
|
||||
output.push_str("```");
|
||||
output.push_str(&path);
|
||||
output.push('\n');
|
||||
for chunk in snapshot.as_rope().chunks() {
|
||||
output.push_str(chunk);
|
||||
}
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
Ok(output)
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no recent buffer found")))
|
||||
}
|
||||
});
|
||||
|
||||
SlashCommandInvocation {
|
||||
output: output.unwrap_or_else(|error| Task::ready(Err(error))),
|
||||
invalidated: invalidate_rx,
|
||||
cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, Model, Task};
|
||||
use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use std::{
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct FileSlashCommand {
|
||||
project: Model<Project>,
|
||||
|
@ -30,7 +31,6 @@ impl FileSlashCommand {
|
|||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
|
@ -40,7 +40,7 @@ impl FileSlashCommand {
|
|||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name,
|
||||
include_root_name: true,
|
||||
directories_only: false,
|
||||
}
|
||||
})
|
||||
|
@ -68,7 +68,11 @@ impl SlashCommand for FileSlashCommand {
|
|||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert an entire file".into()
|
||||
"insert a file".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert file".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
@ -100,36 +104,30 @@ impl SlashCommand for FileSlashCommand {
|
|||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(argument) = argument else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
return Task::ready(Err(anyhow::anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let path = Path::new(argument);
|
||||
let path = PathBuf::from(argument);
|
||||
let abs_path = project.worktrees().find_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.entry_for_path(path)?;
|
||||
worktree.absolutize(path).ok()
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||
worktree.absolutize(&relative_path).ok()
|
||||
});
|
||||
|
||||
let Some(abs_path) = abs_path else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
return Task::ready(Err(anyhow::anyhow!("missing path")));
|
||||
};
|
||||
|
||||
let fs = project.fs().clone();
|
||||
let argument = argument.to_string();
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let text = cx.background_executor().spawn(async move {
|
||||
let content = fs.load(&abs_path).await?;
|
||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||
output.push_str("```");
|
||||
|
@ -140,12 +138,46 @@ impl SlashCommand for FileSlashCommand {
|
|||
output.push('\n');
|
||||
}
|
||||
output.push_str("```");
|
||||
Ok(output)
|
||||
anyhow::Ok(output)
|
||||
});
|
||||
SlashCommandInvocation {
|
||||
output,
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
}
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FilePlaceholder {
|
||||
path: Some(path.clone()),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct FilePlaceholder {
|
||||
pub path: Option<PathBuf>,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl RenderOnce for FilePlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
let title = if let Some(path) = self.path.as_ref() {
|
||||
SharedString::from(path.to_string_lossy().to_string())
|
||||
} else {
|
||||
SharedString::from("untitled")
|
||||
};
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::File))
|
||||
.child(Label::new(title))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
|
151
crates/assistant/src/slash_command/project_command.rs
Normal file
151
crates/assistant/src/slash_command/project_command.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct ProjectSlashCommand;
|
||||
|
||||
impl ProjectSlashCommand {
|
||||
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
|
||||
let buffer = fs.load(path_to_cargo_toml).await?;
|
||||
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
|
||||
|
||||
let mut message = String::new();
|
||||
writeln!(message, "You are in a Rust project.")?;
|
||||
|
||||
if let Some(workspace) = cargo_toml.workspace {
|
||||
writeln!(
|
||||
message,
|
||||
"The project is a Cargo workspace with the following members:"
|
||||
)?;
|
||||
for member in workspace.members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
|
||||
if !workspace.default_members.is_empty() {
|
||||
writeln!(message, "The default members are:")?;
|
||||
for member in workspace.default_members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if !workspace.dependencies.is_empty() {
|
||||
writeln!(
|
||||
message,
|
||||
"The following workspace dependencies are installed:"
|
||||
)?;
|
||||
for dependency in workspace.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
} else if let Some(package) = cargo_toml.package {
|
||||
writeln!(
|
||||
message,
|
||||
"The project name is \"{name}\".",
|
||||
name = package.name
|
||||
)?;
|
||||
|
||||
let description = package
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|description| description.get().ok().cloned());
|
||||
if let Some(description) = description.as_ref() {
|
||||
writeln!(message, "It describes itself as \"{description}\".")?;
|
||||
}
|
||||
|
||||
if !cargo_toml.dependencies.is_empty() {
|
||||
writeln!(message, "The following dependencies are installed:")?;
|
||||
for dependency in cargo_toml.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
Some(Arc::from(
|
||||
project.read(cx).absolute_path(&path, cx)?.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for ProjectSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"project".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert current project context".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert current project context".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let fs = workspace.project().read(cx).fs().clone();
|
||||
let path = Self::path_to_cargo_toml(project, cx);
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let path = path.with_context(|| "Cargo.toml not found")?;
|
||||
Self::build_message(fs, &path).await
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = output.await?;
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileTree))
|
||||
.child(Label::new("Project"))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
})
|
||||
})
|
||||
});
|
||||
output.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompts::PromptLibrary;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
library: Arc<PromptLibrary>,
|
||||
|
@ -26,6 +27,10 @@ impl SlashCommand for PromptSlashCommand {
|
|||
"insert a prompt from the library".into()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
"insert prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -64,32 +69,43 @@ impl SlashCommand for PromptSlashCommand {
|
|||
fn run(
|
||||
self: Arc<Self>,
|
||||
title: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(title) = title else {
|
||||
return SlashCommandInvocation {
|
||||
output: Task::ready(Err(anyhow!("missing prompt name"))),
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
};
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
let library = self.library.clone();
|
||||
let title = title.to_string();
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.find(|prompt| &prompt.1.title().to_string() == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
Ok(prompt.body())
|
||||
let title = SharedString::from(title.to_string());
|
||||
let prompt = cx.background_executor().spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.map(|prompt| (prompt.1.title(), prompt))
|
||||
.find(|(t, _)| t == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
anyhow::Ok(prompt.1.body())
|
||||
}
|
||||
});
|
||||
SlashCommandInvocation {
|
||||
output,
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
}
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let prompt = prompt.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text: prompt,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ path = "src/assistant_slash_command.rs"
|
|||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
parking_lot.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
mod slash_command_registry;
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, Task};
|
||||
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
|
||||
use language::LspAdapterDelegate;
|
||||
|
||||
pub use slash_command_registry::*;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
|
@ -17,6 +14,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn description(&self) -> String;
|
||||
fn tooltip_text(&self) -> String;
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
|
@ -27,35 +25,24 @@ pub trait SlashCommand: 'static + Send + Sync {
|
|||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||
// what the extension API is already expecting.
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation;
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>>;
|
||||
}
|
||||
|
||||
pub struct SlashCommandInvocation {
|
||||
pub output: Task<Result<String>>,
|
||||
pub invalidated: oneshot::Receiver<()>,
|
||||
pub cleanup: SlashCommandCleanup,
|
||||
}
|
||||
pub type RenderFoldPlaceholder = Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
|
||||
|
||||
impl SlashCommandCleanup {
|
||||
pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
|
||||
Self(Some(Box::new(cleanup)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SlashCommandCleanup {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cleanup) = self.0.take() {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub render_placeholder: RenderFoldPlaceholder,
|
||||
}
|
||||
|
|
|
@ -305,6 +305,7 @@ impl MessageEditor {
|
|||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
confirm: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
|
@ -36,7 +36,13 @@ impl FlapSnapshot {
|
|||
while let Some(item) = cursor.item() {
|
||||
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
|
||||
Ordering::Less => cursor.next(snapshot),
|
||||
Ordering::Equal => return Some(&item.flap),
|
||||
Ordering::Equal => {
|
||||
if item.flap.range.start.is_valid(snapshot) {
|
||||
return Some(&item.flap);
|
||||
} else {
|
||||
cursor.next(snapshot);
|
||||
}
|
||||
}
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ pub struct FoldPlaceholder {
|
|||
pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
|
||||
/// If true, the element is constrained to the shaped width of an ellipsis.
|
||||
pub constrain_width: bool,
|
||||
/// If true, merges the fold with an adjacent one.
|
||||
pub merge_adjacent: bool,
|
||||
}
|
||||
|
||||
impl FoldPlaceholder {
|
||||
|
@ -30,6 +32,7 @@ impl FoldPlaceholder {
|
|||
Self {
|
||||
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
|
||||
constrain_width: true,
|
||||
merge_adjacent: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -374,8 +377,11 @@ impl FoldMap {
|
|||
|
||||
assert!(fold_range.start.0 >= sum.input.len);
|
||||
|
||||
while folds.peek().map_or(false, |(_, next_fold_range)| {
|
||||
next_fold_range.start <= fold_range.end
|
||||
while folds.peek().map_or(false, |(next_fold, next_fold_range)| {
|
||||
next_fold_range.start < fold_range.end
|
||||
|| (next_fold_range.start == fold_range.end
|
||||
&& fold.placeholder.merge_adjacent
|
||||
&& next_fold.placeholder.merge_adjacent)
|
||||
}) {
|
||||
let (_, next_fold_range) = folds.next().unwrap();
|
||||
if next_fold_range.end > fold_range.end {
|
||||
|
|
|
@ -1628,6 +1628,7 @@ impl Editor {
|
|||
})
|
||||
.into_any()
|
||||
}),
|
||||
merge_adjacent: true,
|
||||
};
|
||||
let display_map = cx.new_model(|cx| {
|
||||
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
|
||||
|
@ -3905,6 +3906,7 @@ impl Editor {
|
|||
|
||||
let snippet;
|
||||
let text;
|
||||
|
||||
if completion.is_snippet() {
|
||||
snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
|
||||
text = snippet.as_ref().unwrap().text.clone();
|
||||
|
@ -3998,6 +4000,10 @@ impl Editor {
|
|||
this.refresh_inline_completion(true, cx);
|
||||
});
|
||||
|
||||
if let Some(confirm) = completion.confirm.as_ref() {
|
||||
(confirm)(cx);
|
||||
}
|
||||
|
||||
let provider = self.completion_provider.as_ref()?;
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
|
|
|
@ -3908,7 +3908,7 @@ enum LineFragment {
|
|||
Text(ShapedLine),
|
||||
Element {
|
||||
element: Option<AnyElement>,
|
||||
width: Pixels,
|
||||
size: Size<Pixels>,
|
||||
len: usize,
|
||||
},
|
||||
}
|
||||
|
@ -3917,9 +3917,9 @@ impl fmt::Debug for LineFragment {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
|
||||
LineFragment::Element { width, len, .. } => f
|
||||
LineFragment::Element { size, len, .. } => f
|
||||
.debug_struct("Element")
|
||||
.field("width", width)
|
||||
.field("size", size)
|
||||
.field("len", len)
|
||||
.finish(),
|
||||
}
|
||||
|
@ -3999,7 +3999,7 @@ impl LineWithInvisibles {
|
|||
len += highlighted_chunk.text.len();
|
||||
fragments.push(LineFragment::Element {
|
||||
element: Some(element),
|
||||
width: size.width,
|
||||
size,
|
||||
len: highlighted_chunk.text.len(),
|
||||
});
|
||||
} else {
|
||||
|
@ -4112,13 +4112,18 @@ impl LineWithInvisibles {
|
|||
LineFragment::Text(line) => {
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { element, width, .. } => {
|
||||
LineFragment::Element { element, size, .. } => {
|
||||
let mut element = element
|
||||
.take()
|
||||
.expect("you can't prepaint LineWithInvisibles twice");
|
||||
element.prepaint_at(fragment_origin, cx);
|
||||
|
||||
// Center the element vertically within the line.
|
||||
let mut element_origin = fragment_origin;
|
||||
element_origin.y += (line_height - size.height) / 2.;
|
||||
element.prepaint_at(element_origin, cx);
|
||||
line_elements.push(element);
|
||||
fragment_origin.x += *width;
|
||||
|
||||
fragment_origin.x += size.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4146,8 +4151,8 @@ impl LineWithInvisibles {
|
|||
line.paint(fragment_origin, line_height, cx).log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { width, .. } => {
|
||||
fragment_origin.x += *width;
|
||||
LineFragment::Element { size, .. } => {
|
||||
fragment_origin.x += size.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4225,12 +4230,12 @@ impl LineWithInvisibles {
|
|||
fragment_start_x += shaped_line.width;
|
||||
fragment_start_index = fragment_end_index;
|
||||
}
|
||||
LineFragment::Element { len, width, .. } => {
|
||||
LineFragment::Element { len, size, .. } => {
|
||||
let fragment_end_index = fragment_start_index + len;
|
||||
if index < fragment_end_index {
|
||||
return fragment_start_x;
|
||||
}
|
||||
fragment_start_x += *width;
|
||||
fragment_start_x += size.width;
|
||||
fragment_start_index = fragment_end_index;
|
||||
}
|
||||
}
|
||||
|
@ -4255,8 +4260,8 @@ impl LineWithInvisibles {
|
|||
fragment_start_x = fragment_end_x;
|
||||
fragment_start_index += shaped_line.len;
|
||||
}
|
||||
LineFragment::Element { len, width, .. } => {
|
||||
let fragment_end_x = fragment_start_x + *width;
|
||||
LineFragment::Element { len, size, .. } => {
|
||||
let fragment_end_x = fragment_start_x + size.width;
|
||||
if x < fragment_end_x {
|
||||
return Some(fragment_start_index);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ wasmtime.workspace = true
|
|||
wasmtime-wasi.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wit-component.workspace = true
|
||||
workspace.workspace = true
|
||||
task.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
|
||||
|
@ -58,3 +59,4 @@ fs = { workspace = true, features = ["test-support"] }
|
|||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -133,6 +133,7 @@ impl LanguageServerManifestEntry {
|
|||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct SlashCommandManifestEntry {
|
||||
pub description: String,
|
||||
pub tooltip_text: String,
|
||||
pub requires_argument: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||
use futures::channel::oneshot;
|
||||
use futures::FutureExt;
|
||||
use gpui::{AppContext, Task};
|
||||
use language::LspAdapterDelegate;
|
||||
use wasmtime_wasi::WasiView;
|
||||
|
||||
use crate::wasm_host::{WasmExtension, WasmHost};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput};
|
||||
use futures::FutureExt;
|
||||
use gpui::{AppContext, IntoElement, Task, WeakView, WindowContext};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use wasmtime_wasi::WasiView;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ExtensionSlashCommand {
|
||||
pub(crate) extension: WasmExtension,
|
||||
|
@ -27,6 +24,10 @@ impl SlashCommand for ExtensionSlashCommand {
|
|||
self.command.description.clone()
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
self.command.tooltip_text.clone()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
self.command.requires_argument
|
||||
}
|
||||
|
@ -43,11 +44,11 @@ impl SlashCommand for ExtensionSlashCommand {
|
|||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AppContext,
|
||||
) -> SlashCommandInvocation {
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let argument = argument.map(|arg| arg.to_string());
|
||||
|
||||
let output = cx.background_executor().spawn(async move {
|
||||
let output = self
|
||||
.extension
|
||||
|
@ -72,14 +73,16 @@ impl SlashCommand for ExtensionSlashCommand {
|
|||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name))
|
||||
});
|
||||
|
||||
SlashCommandInvocation {
|
||||
output,
|
||||
invalidated: oneshot::channel().1,
|
||||
cleanup: SlashCommandCleanup::default(),
|
||||
}
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let output = output.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text: output,
|
||||
render_placeholder: Arc::new(|_, _, _| {
|
||||
"TODO: Extension command output".into_any_element()
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1183,6 +1183,7 @@ impl ExtensionStore {
|
|||
command: crate::wit::SlashCommand {
|
||||
name: slash_command_name.to_string(),
|
||||
description: slash_command.description.to_string(),
|
||||
tooltip_text: slash_command.tooltip_text.to_string(),
|
||||
requires_argument: slash_command.requires_argument,
|
||||
},
|
||||
extension: wasm_extension.clone(),
|
||||
|
|
|
@ -5,6 +5,8 @@ interface slash-command {
|
|||
name: string,
|
||||
/// The description of the slash command.
|
||||
description: string,
|
||||
/// The tooltip text to display for the run button.
|
||||
tooltip-text: string,
|
||||
/// Whether this slash command requires an argument.
|
||||
requires-argument: bool,
|
||||
}
|
||||
|
|
|
@ -291,6 +291,8 @@ impl Interactivity {
|
|||
let action = action.downcast_ref().unwrap();
|
||||
if phase == DispatchPhase::Capture {
|
||||
(listener)(action, cx)
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
|
|
@ -36,7 +36,7 @@ use git::{blame::Blame, repository::GitRepository};
|
|||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
|
||||
EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel,
|
||||
EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
|
@ -407,7 +407,7 @@ pub struct InlayHint {
|
|||
}
|
||||
|
||||
/// A completion provided by a language server
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct Completion {
|
||||
/// The range of the buffer that will be replaced.
|
||||
pub old_range: Range<Anchor>,
|
||||
|
@ -421,6 +421,21 @@ pub struct Completion {
|
|||
pub documentation: Option<Documentation>,
|
||||
/// The raw completion provided by the language server.
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
/// An optional callback to invoke when this completion is confirmed.
|
||||
pub confirm: Option<Arc<dyn Send + Sync + Fn(&mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Completion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Completion")
|
||||
.field("old_range", &self.old_range)
|
||||
.field("new_text", &self.new_text)
|
||||
.field("label", &self.label)
|
||||
.field("server_id", &self.server_id)
|
||||
.field("documentation", &self.documentation)
|
||||
.field("lsp_completion", &self.lsp_completion)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A completion provided by a language server
|
||||
|
@ -2029,6 +2044,30 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn open_buffer_for_full_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
if let Some(worktree_name) = path.components().next() {
|
||||
let worktree = self.worktrees().find(|worktree| {
|
||||
OsStr::new(worktree.read(cx).root_name()) == worktree_name.as_os_str()
|
||||
});
|
||||
if let Some(worktree) = worktree {
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree_root_path = Path::new(worktree.root_name());
|
||||
if let Ok(path) = path.strip_prefix(worktree_root_path) {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: path.into(),
|
||||
};
|
||||
return self.open_buffer(project_path, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::ready(Err(anyhow!("buffer not found for {:?}", path)))
|
||||
}
|
||||
|
||||
pub fn open_local_buffer(
|
||||
&mut self,
|
||||
abs_path: impl AsRef<Path>,
|
||||
|
@ -9212,6 +9251,7 @@ impl Project {
|
|||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
confirm: None,
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
|
@ -10883,6 +10923,7 @@ async fn populate_labels_for_completions(
|
|||
server_id: completion.server_id,
|
||||
documentation,
|
||||
lsp_completion,
|
||||
confirm: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,6 +184,7 @@ pub enum IconName {
|
|||
Tab,
|
||||
Terminal,
|
||||
Trash,
|
||||
TriangleRight,
|
||||
Update,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
|
@ -303,6 +304,7 @@ impl IconName {
|
|||
IconName::Tab => "icons/tab.svg",
|
||||
IconName::Terminal => "icons/terminal.svg",
|
||||
IconName::Trash => "icons/trash.svg",
|
||||
IconName::TriangleRight => "icons/triangle_right.svg",
|
||||
IconName::Update => "icons/update.svg",
|
||||
IconName::WholeWord => "icons/word_search.svg",
|
||||
IconName::XCircle => "icons/error.svg",
|
||||
|
|
|
@ -17,3 +17,4 @@ commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
|
|||
[slash_commands.gleam-project]
|
||||
description = "Returns information about the current Gleam project."
|
||||
requires_argument = false
|
||||
tooltip_text = "Insert Gleam project data"
|
||||
|
|
Loading…
Reference in a new issue