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",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures 0.3.28",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3823,6 +3823,7 @@ dependencies = [
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wasmtime-wasi",
|
"wasmtime-wasi",
|
||||||
"wit-component",
|
"wit-component",
|
||||||
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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-s": "workspace::Save",
|
||||||
"ctrl->": "assistant::QuoteSelection",
|
"ctrl->": "assistant::QuoteSelection",
|
||||||
"shift-enter": "assistant::Split",
|
"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-s": "workspace::Save",
|
||||||
"cmd->": "assistant::QuoteSelection",
|
"cmd->": "assistant::QuoteSelection",
|
||||||
"shift-enter": "assistant::Split",
|
"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_panel;
|
||||||
pub mod assistant_settings;
|
pub mod assistant_settings;
|
||||||
mod codegen;
|
mod codegen;
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
mod omit_ranges;
|
|
||||||
mod prompts;
|
mod prompts;
|
||||||
mod saved_conversation;
|
mod saved_conversation;
|
||||||
mod search;
|
mod search;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
|
|
||||||
use ambient_context::AmbientContextSnapshot;
|
|
||||||
pub use assistant_panel::AssistantPanel;
|
pub use assistant_panel::AssistantPanel;
|
||||||
|
|
||||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
|
@ -38,7 +36,8 @@ actions!(
|
||||||
InsertActivePrompt,
|
InsertActivePrompt,
|
||||||
ToggleIncludeConversation,
|
ToggleIncludeConversation,
|
||||||
ToggleHistory,
|
ToggleHistory,
|
||||||
ApplyEdit
|
ApplyEdit,
|
||||||
|
ConfirmCommand
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -188,9 +187,6 @@ pub struct LanguageModelChoiceDelta {
|
||||||
struct MessageMetadata {
|
struct MessageMetadata {
|
||||||
role: Role,
|
role: Role,
|
||||||
status: MessageStatus,
|
status: MessageStatus,
|
||||||
// TODO: Delete this
|
|
||||||
#[serde(skip)]
|
|
||||||
ambient_context: AmbientContextSnapshot,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[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;
|
use anyhow::Result;
|
||||||
|
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||||
use editor::{CompletionProvider, Editor};
|
use editor::{CompletionProvider, Editor};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
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 language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
|
@ -12,18 +14,18 @@ use std::{
|
||||||
Arc,
|
Arc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub use assistant_slash_command::{
|
pub mod active_command;
|
||||||
SlashCommand, SlashCommandCleanup, SlashCommandInvocation, SlashCommandRegistry,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod current_file_command;
|
|
||||||
pub mod file_command;
|
pub mod file_command;
|
||||||
|
pub mod project_command;
|
||||||
pub mod prompt_command;
|
pub mod prompt_command;
|
||||||
|
|
||||||
pub(crate) struct SlashCommandCompletionProvider {
|
pub(crate) struct SlashCommandCompletionProvider {
|
||||||
|
editor: WeakView<ConversationEditor>,
|
||||||
commands: Arc<SlashCommandRegistry>,
|
commands: Arc<SlashCommandRegistry>,
|
||||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct SlashCommandLine {
|
pub(crate) struct SlashCommandLine {
|
||||||
|
@ -34,18 +36,25 @@ pub(crate) struct SlashCommandLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlashCommandCompletionProvider {
|
impl SlashCommandCompletionProvider {
|
||||||
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
|
pub fn new(
|
||||||
|
editor: WeakView<ConversationEditor>,
|
||||||
|
commands: Arc<SlashCommandRegistry>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||||
|
editor,
|
||||||
commands,
|
commands,
|
||||||
|
workspace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_command_name(
|
fn complete_command_name(
|
||||||
&self,
|
&self,
|
||||||
command_name: &str,
|
command_name: &str,
|
||||||
range: Range<Anchor>,
|
command_range: Range<Anchor>,
|
||||||
cx: &mut AppContext,
|
name_range: Range<Anchor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<Vec<project::Completion>>> {
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
let candidates = self
|
let candidates = self
|
||||||
.commands
|
.commands
|
||||||
|
@ -60,6 +69,8 @@ impl SlashCommandCompletionProvider {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let commands = self.commands.clone();
|
let commands = self.commands.clone();
|
||||||
let command_name = command_name.to_string();
|
let command_name = command_name.to_string();
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
let executor = cx.background_executor().clone();
|
let executor = cx.background_executor().clone();
|
||||||
executor.clone().spawn(async move {
|
executor.clone().spawn(async move {
|
||||||
let matches = match_strings(
|
let matches = match_strings(
|
||||||
|
@ -77,17 +88,37 @@ impl SlashCommandCompletionProvider {
|
||||||
.filter_map(|mat| {
|
.filter_map(|mat| {
|
||||||
let command = commands.command(&mat.string)?;
|
let command = commands.command(&mat.string)?;
|
||||||
let mut new_text = mat.string.clone();
|
let mut new_text = mat.string.clone();
|
||||||
if command.requires_argument() {
|
let requires_argument = command.requires_argument();
|
||||||
|
if requires_argument {
|
||||||
new_text.push(' ');
|
new_text.push(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(project::Completion {
|
Some(project::Completion {
|
||||||
old_range: range.clone(),
|
old_range: name_range.clone(),
|
||||||
documentation: Some(Documentation::SingleLine(command.description())),
|
documentation: Some(Documentation::SingleLine(command.description())),
|
||||||
new_text,
|
new_text,
|
||||||
label: CodeLabel::plain(mat.string, None),
|
label: CodeLabel::plain(mat.string.clone(), None),
|
||||||
server_id: LanguageServerId(0),
|
server_id: LanguageServerId(0),
|
||||||
lsp_completion: Default::default(),
|
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())
|
.collect())
|
||||||
|
@ -98,8 +129,9 @@ impl SlashCommandCompletionProvider {
|
||||||
&self,
|
&self,
|
||||||
command_name: &str,
|
command_name: &str,
|
||||||
argument: String,
|
argument: String,
|
||||||
range: Range<Anchor>,
|
command_range: Range<Anchor>,
|
||||||
cx: &mut AppContext,
|
argument_range: Range<Anchor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<Vec<project::Completion>>> {
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
let mut flag = self.cancel_flag.lock();
|
let mut flag = self.cancel_flag.lock();
|
||||||
|
@ -108,17 +140,39 @@ impl SlashCommandCompletionProvider {
|
||||||
|
|
||||||
if let Some(command) = self.commands.command(command_name) {
|
if let Some(command) = self.commands.command(command_name) {
|
||||||
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
|
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 {
|
cx.background_executor().spawn(async move {
|
||||||
Ok(completions
|
Ok(completions
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|arg| project::Completion {
|
.map(|arg| project::Completion {
|
||||||
old_range: range.clone(),
|
old_range: argument_range.clone(),
|
||||||
label: CodeLabel::plain(arg.clone(), None),
|
label: CodeLabel::plain(arg.clone(), None),
|
||||||
new_text: arg.clone(),
|
new_text: arg.clone(),
|
||||||
documentation: None,
|
documentation: None,
|
||||||
server_id: LanguageServerId(0),
|
server_id: LanguageServerId(0),
|
||||||
lsp_completion: Default::default(),
|
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())
|
.collect())
|
||||||
})
|
})
|
||||||
|
@ -136,25 +190,44 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||||
buffer_position: Anchor,
|
buffer_position: Anchor,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> Task<Result<Vec<project::Completion>>> {
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
let task = buffer.update(cx, |buffer, cx| {
|
let Some((name, argument, command_range, argument_range)) =
|
||||||
let position = buffer_position.to_point(buffer);
|
buffer.update(cx, |buffer, _cx| {
|
||||||
let line_start = Point::new(position.row, 0);
|
let position = buffer_position.to_point(buffer);
|
||||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
let line_start = Point::new(position.row, 0);
|
||||||
let line = lines.next()?;
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||||
let call = SlashCommandLine::parse(line)?;
|
let line = lines.next()?;
|
||||||
|
let call = SlashCommandLine::parse(line)?;
|
||||||
|
|
||||||
let name = &line[call.name.clone()];
|
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
|
||||||
if let Some(argument) = call.argument {
|
let command_range_end = Point::new(
|
||||||
let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
position.row,
|
||||||
let argument = line[argument.clone()].to_string();
|
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
|
||||||
Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
|
);
|
||||||
} else {
|
let command_range = buffer.anchor_after(command_range_start)
|
||||||
let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
..buffer.anchor_after(command_range_end);
|
||||||
Some(self.complete_command_name(name, start..buffer_position, cx))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
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 anyhow::Result;
|
||||||
use futures::channel::oneshot;
|
|
||||||
use fuzzy::PathMatch;
|
use fuzzy::PathMatch;
|
||||||
use gpui::{AppContext, Model, Task};
|
use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::LspAdapterDelegate;
|
||||||
use project::{PathMatchCandidateSet, Project};
|
use project::{PathMatchCandidateSet, Project};
|
||||||
use std::{
|
use std::{
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
|
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct FileSlashCommand {
|
pub(crate) struct FileSlashCommand {
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
|
@ -30,7 +31,6 @@ impl FileSlashCommand {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.visible_worktrees(cx)
|
.visible_worktrees(cx)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let include_root_name = worktrees.len() > 1;
|
|
||||||
let candidate_sets = worktrees
|
let candidate_sets = worktrees
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|worktree| {
|
.map(|worktree| {
|
||||||
|
@ -40,7 +40,7 @@ impl FileSlashCommand {
|
||||||
include_ignored: worktree
|
include_ignored: worktree
|
||||||
.root_entry()
|
.root_entry()
|
||||||
.map_or(false, |entry| entry.is_ignored),
|
.map_or(false, |entry| entry.is_ignored),
|
||||||
include_root_name,
|
include_root_name: true,
|
||||||
directories_only: false,
|
directories_only: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -68,7 +68,11 @@ impl SlashCommand for FileSlashCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
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 {
|
fn requires_argument(&self) -> bool {
|
||||||
|
@ -100,36 +104,30 @@ impl SlashCommand for FileSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||||
cx: &mut AppContext,
|
cx: &mut WindowContext,
|
||||||
) -> SlashCommandInvocation {
|
) -> Task<Result<SlashCommandOutput>> {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
let Some(argument) = argument else {
|
let Some(argument) = argument else {
|
||||||
return SlashCommandInvocation {
|
return Task::ready(Err(anyhow::anyhow!("missing path")));
|
||||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
|
||||||
invalidated: oneshot::channel().1,
|
|
||||||
cleanup: SlashCommandCleanup::default(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = Path::new(argument);
|
let path = PathBuf::from(argument);
|
||||||
let abs_path = project.worktrees().find_map(|worktree| {
|
let abs_path = project.worktrees().find_map(|worktree| {
|
||||||
let worktree = worktree.read(cx);
|
let worktree = worktree.read(cx);
|
||||||
worktree.entry_for_path(path)?;
|
let worktree_root_path = Path::new(worktree.root_name());
|
||||||
worktree.absolutize(path).ok()
|
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
|
||||||
|
worktree.absolutize(&relative_path).ok()
|
||||||
});
|
});
|
||||||
|
|
||||||
let Some(abs_path) = abs_path else {
|
let Some(abs_path) = abs_path else {
|
||||||
return SlashCommandInvocation {
|
return Task::ready(Err(anyhow::anyhow!("missing path")));
|
||||||
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
|
|
||||||
invalidated: oneshot::channel().1,
|
|
||||||
cleanup: SlashCommandCleanup::default(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let fs = project.fs().clone();
|
let fs = project.fs().clone();
|
||||||
let argument = argument.to_string();
|
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 content = fs.load(&abs_path).await?;
|
||||||
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
let mut output = String::with_capacity(argument.len() + content.len() + 9);
|
||||||
output.push_str("```");
|
output.push_str("```");
|
||||||
|
@ -140,12 +138,46 @@ impl SlashCommand for FileSlashCommand {
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
output.push_str("```");
|
output.push_str("```");
|
||||||
Ok(output)
|
anyhow::Ok(output)
|
||||||
});
|
});
|
||||||
SlashCommandInvocation {
|
cx.foreground_executor().spawn(async move {
|
||||||
output,
|
let text = text.await?;
|
||||||
invalidated: oneshot::channel().1,
|
Ok(SlashCommandOutput {
|
||||||
cleanup: SlashCommandCleanup::default(),
|
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 crate::prompts::PromptLibrary;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use futures::channel::oneshot;
|
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{AppContext, Task};
|
use gpui::{AppContext, Task, WeakView};
|
||||||
use language::LspAdapterDelegate;
|
use language::LspAdapterDelegate;
|
||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub(crate) struct PromptSlashCommand {
|
pub(crate) struct PromptSlashCommand {
|
||||||
library: Arc<PromptLibrary>,
|
library: Arc<PromptLibrary>,
|
||||||
|
@ -26,6 +27,10 @@ impl SlashCommand for PromptSlashCommand {
|
||||||
"insert a prompt from the library".into()
|
"insert a prompt from the library".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tooltip_text(&self) -> String {
|
||||||
|
"insert prompt".into()
|
||||||
|
}
|
||||||
|
|
||||||
fn requires_argument(&self) -> bool {
|
fn requires_argument(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -64,32 +69,43 @@ impl SlashCommand for PromptSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||||
cx: &mut AppContext,
|
cx: &mut WindowContext,
|
||||||
) -> SlashCommandInvocation {
|
) -> Task<Result<SlashCommandOutput>> {
|
||||||
let Some(title) = title else {
|
let Some(title) = title else {
|
||||||
return SlashCommandInvocation {
|
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||||
output: Task::ready(Err(anyhow!("missing prompt name"))),
|
|
||||||
invalidated: oneshot::channel().1,
|
|
||||||
cleanup: SlashCommandCleanup::default(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let library = self.library.clone();
|
let library = self.library.clone();
|
||||||
let title = title.to_string();
|
let title = SharedString::from(title.to_string());
|
||||||
let output = cx.background_executor().spawn(async move {
|
let prompt = cx.background_executor().spawn({
|
||||||
let prompt = library
|
let title = title.clone();
|
||||||
.prompts()
|
async move {
|
||||||
.into_iter()
|
let prompt = library
|
||||||
.find(|prompt| &prompt.1.title().to_string() == &title)
|
.prompts()
|
||||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
.into_iter()
|
||||||
.1;
|
.map(|prompt| (prompt.1.title(), prompt))
|
||||||
Ok(prompt.body())
|
.find(|(t, _)| t == &title)
|
||||||
|
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||||
|
.1;
|
||||||
|
anyhow::Ok(prompt.1.body())
|
||||||
|
}
|
||||||
});
|
});
|
||||||
SlashCommandInvocation {
|
cx.foreground_executor().spawn(async move {
|
||||||
output,
|
let prompt = prompt.await?;
|
||||||
invalidated: oneshot::channel().1,
|
Ok(SlashCommandOutput {
|
||||||
cleanup: SlashCommandCleanup::default(),
|
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
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
futures.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
mod slash_command_registry;
|
mod slash_command_registry;
|
||||||
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::channel::oneshot;
|
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
|
||||||
use gpui::{AppContext, Task};
|
|
||||||
use language::LspAdapterDelegate;
|
use language::LspAdapterDelegate;
|
||||||
|
|
||||||
pub use slash_command_registry::*;
|
pub use slash_command_registry::*;
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
SlashCommandRegistry::default_global(cx);
|
SlashCommandRegistry::default_global(cx);
|
||||||
|
@ -17,6 +14,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
pub trait SlashCommand: 'static + Send + Sync {
|
pub trait SlashCommand: 'static + Send + Sync {
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn description(&self) -> String;
|
fn description(&self) -> String;
|
||||||
|
fn tooltip_text(&self) -> String;
|
||||||
fn complete_argument(
|
fn complete_argument(
|
||||||
&self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
|
@ -27,35 +25,24 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||||
// what the extension API is already expecting.
|
// what the extension API is already expecting.
|
||||||
//
|
//
|
||||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||||
// perhaps another kind of delegate is needed here.
|
// perhaps another kind of delegate is needed here.
|
||||||
delegate: Arc<dyn LspAdapterDelegate>,
|
delegate: Arc<dyn LspAdapterDelegate>,
|
||||||
cx: &mut AppContext,
|
cx: &mut WindowContext,
|
||||||
) -> SlashCommandInvocation;
|
) -> Task<Result<SlashCommandOutput>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SlashCommandInvocation {
|
pub type RenderFoldPlaceholder = Arc<
|
||||||
pub output: Task<Result<String>>,
|
dyn Send
|
||||||
pub invalidated: oneshot::Receiver<()>,
|
+ Sync
|
||||||
pub cleanup: SlashCommandCleanup,
|
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||||
}
|
>;
|
||||||
|
|
||||||
#[derive(Default)]
|
pub struct SlashCommandOutput {
|
||||||
pub struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
|
pub text: String,
|
||||||
|
pub render_placeholder: RenderFoldPlaceholder,
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,6 +305,7 @@ impl MessageEditor {
|
||||||
documentation: None,
|
documentation: None,
|
||||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||||
|
confirm: None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
|
@ -36,7 +36,13 @@ impl FlapSnapshot {
|
||||||
while let Some(item) = cursor.item() {
|
while let Some(item) = cursor.item() {
|
||||||
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
|
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
|
||||||
Ordering::Less => cursor.next(snapshot),
|
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,
|
Ordering::Greater => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ pub struct FoldPlaceholder {
|
||||||
pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
|
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.
|
/// If true, the element is constrained to the shaped width of an ellipsis.
|
||||||
pub constrain_width: bool,
|
pub constrain_width: bool,
|
||||||
|
/// If true, merges the fold with an adjacent one.
|
||||||
|
pub merge_adjacent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FoldPlaceholder {
|
impl FoldPlaceholder {
|
||||||
|
@ -30,6 +32,7 @@ impl FoldPlaceholder {
|
||||||
Self {
|
Self {
|
||||||
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
|
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
|
||||||
constrain_width: true,
|
constrain_width: true,
|
||||||
|
merge_adjacent: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,8 +377,11 @@ impl FoldMap {
|
||||||
|
|
||||||
assert!(fold_range.start.0 >= sum.input.len);
|
assert!(fold_range.start.0 >= sum.input.len);
|
||||||
|
|
||||||
while folds.peek().map_or(false, |(_, next_fold_range)| {
|
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
|
||||||
|
|| (next_fold_range.start == fold_range.end
|
||||||
|
&& fold.placeholder.merge_adjacent
|
||||||
|
&& next_fold.placeholder.merge_adjacent)
|
||||||
}) {
|
}) {
|
||||||
let (_, next_fold_range) = folds.next().unwrap();
|
let (_, next_fold_range) = folds.next().unwrap();
|
||||||
if next_fold_range.end > fold_range.end {
|
if next_fold_range.end > fold_range.end {
|
||||||
|
|
|
@ -1628,6 +1628,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.into_any()
|
.into_any()
|
||||||
}),
|
}),
|
||||||
|
merge_adjacent: true,
|
||||||
};
|
};
|
||||||
let display_map = cx.new_model(|cx| {
|
let display_map = cx.new_model(|cx| {
|
||||||
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
|
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
|
||||||
|
@ -3905,6 +3906,7 @@ impl Editor {
|
||||||
|
|
||||||
let snippet;
|
let snippet;
|
||||||
let text;
|
let text;
|
||||||
|
|
||||||
if completion.is_snippet() {
|
if completion.is_snippet() {
|
||||||
snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
|
snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
|
||||||
text = snippet.as_ref().unwrap().text.clone();
|
text = snippet.as_ref().unwrap().text.clone();
|
||||||
|
@ -3998,6 +4000,10 @@ impl Editor {
|
||||||
this.refresh_inline_completion(true, cx);
|
this.refresh_inline_completion(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(confirm) = completion.confirm.as_ref() {
|
||||||
|
(confirm)(cx);
|
||||||
|
}
|
||||||
|
|
||||||
let provider = self.completion_provider.as_ref()?;
|
let provider = self.completion_provider.as_ref()?;
|
||||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||||
buffer_handle,
|
buffer_handle,
|
||||||
|
|
|
@ -3908,7 +3908,7 @@ enum LineFragment {
|
||||||
Text(ShapedLine),
|
Text(ShapedLine),
|
||||||
Element {
|
Element {
|
||||||
element: Option<AnyElement>,
|
element: Option<AnyElement>,
|
||||||
width: Pixels,
|
size: Size<Pixels>,
|
||||||
len: usize,
|
len: usize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -3917,9 +3917,9 @@ impl fmt::Debug for LineFragment {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
|
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")
|
.debug_struct("Element")
|
||||||
.field("width", width)
|
.field("size", size)
|
||||||
.field("len", len)
|
.field("len", len)
|
||||||
.finish(),
|
.finish(),
|
||||||
}
|
}
|
||||||
|
@ -3999,7 +3999,7 @@ impl LineWithInvisibles {
|
||||||
len += highlighted_chunk.text.len();
|
len += highlighted_chunk.text.len();
|
||||||
fragments.push(LineFragment::Element {
|
fragments.push(LineFragment::Element {
|
||||||
element: Some(element),
|
element: Some(element),
|
||||||
width: size.width,
|
size,
|
||||||
len: highlighted_chunk.text.len(),
|
len: highlighted_chunk.text.len(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -4112,13 +4112,18 @@ impl LineWithInvisibles {
|
||||||
LineFragment::Text(line) => {
|
LineFragment::Text(line) => {
|
||||||
fragment_origin.x += line.width;
|
fragment_origin.x += line.width;
|
||||||
}
|
}
|
||||||
LineFragment::Element { element, width, .. } => {
|
LineFragment::Element { element, size, .. } => {
|
||||||
let mut element = element
|
let mut element = element
|
||||||
.take()
|
.take()
|
||||||
.expect("you can't prepaint LineWithInvisibles twice");
|
.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);
|
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();
|
line.paint(fragment_origin, line_height, cx).log_err();
|
||||||
fragment_origin.x += line.width;
|
fragment_origin.x += line.width;
|
||||||
}
|
}
|
||||||
LineFragment::Element { width, .. } => {
|
LineFragment::Element { size, .. } => {
|
||||||
fragment_origin.x += *width;
|
fragment_origin.x += size.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4225,12 +4230,12 @@ impl LineWithInvisibles {
|
||||||
fragment_start_x += shaped_line.width;
|
fragment_start_x += shaped_line.width;
|
||||||
fragment_start_index = fragment_end_index;
|
fragment_start_index = fragment_end_index;
|
||||||
}
|
}
|
||||||
LineFragment::Element { len, width, .. } => {
|
LineFragment::Element { len, size, .. } => {
|
||||||
let fragment_end_index = fragment_start_index + len;
|
let fragment_end_index = fragment_start_index + len;
|
||||||
if index < fragment_end_index {
|
if index < fragment_end_index {
|
||||||
return fragment_start_x;
|
return fragment_start_x;
|
||||||
}
|
}
|
||||||
fragment_start_x += *width;
|
fragment_start_x += size.width;
|
||||||
fragment_start_index = fragment_end_index;
|
fragment_start_index = fragment_end_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4255,8 +4260,8 @@ impl LineWithInvisibles {
|
||||||
fragment_start_x = fragment_end_x;
|
fragment_start_x = fragment_end_x;
|
||||||
fragment_start_index += shaped_line.len;
|
fragment_start_index += shaped_line.len;
|
||||||
}
|
}
|
||||||
LineFragment::Element { len, width, .. } => {
|
LineFragment::Element { len, size, .. } => {
|
||||||
let fragment_end_x = fragment_start_x + *width;
|
let fragment_end_x = fragment_start_x + size.width;
|
||||||
if x < fragment_end_x {
|
if x < fragment_end_x {
|
||||||
return Some(fragment_start_index);
|
return Some(fragment_start_index);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ wasmtime.workspace = true
|
||||||
wasmtime-wasi.workspace = true
|
wasmtime-wasi.workspace = true
|
||||||
wasmparser.workspace = true
|
wasmparser.workspace = true
|
||||||
wit-component.workspace = true
|
wit-component.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
serde_json_lenient.workspace = true
|
serde_json_lenient.workspace = true
|
||||||
|
|
||||||
|
@ -58,3 +59,4 @@ fs = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
project = { 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)]
|
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||||
pub struct SlashCommandManifestEntry {
|
pub struct SlashCommandManifestEntry {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
pub tooltip_text: String,
|
||||||
pub requires_argument: bool,
|
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 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 struct ExtensionSlashCommand {
|
||||||
pub(crate) extension: WasmExtension,
|
pub(crate) extension: WasmExtension,
|
||||||
|
@ -27,6 +24,10 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||||
self.command.description.clone()
|
self.command.description.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tooltip_text(&self) -> String {
|
||||||
|
self.command.tooltip_text.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn requires_argument(&self) -> bool {
|
fn requires_argument(&self) -> bool {
|
||||||
self.command.requires_argument
|
self.command.requires_argument
|
||||||
}
|
}
|
||||||
|
@ -43,11 +44,11 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||||
fn run(
|
fn run(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
delegate: Arc<dyn LspAdapterDelegate>,
|
delegate: Arc<dyn LspAdapterDelegate>,
|
||||||
cx: &mut AppContext,
|
cx: &mut WindowContext,
|
||||||
) -> SlashCommandInvocation {
|
) -> Task<Result<SlashCommandOutput>> {
|
||||||
let argument = argument.map(|arg| arg.to_string());
|
let argument = argument.map(|arg| arg.to_string());
|
||||||
|
|
||||||
let output = cx.background_executor().spawn(async move {
|
let output = cx.background_executor().spawn(async move {
|
||||||
let output = self
|
let output = self
|
||||||
.extension
|
.extension
|
||||||
|
@ -72,14 +73,16 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name))
|
output.ok_or_else(|| anyhow!("no output from command: {}", self.command.name))
|
||||||
});
|
});
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
SlashCommandInvocation {
|
let output = output.await?;
|
||||||
output,
|
Ok(SlashCommandOutput {
|
||||||
invalidated: oneshot::channel().1,
|
text: output,
|
||||||
cleanup: SlashCommandCleanup::default(),
|
render_placeholder: Arc::new(|_, _, _| {
|
||||||
}
|
"TODO: Extension command output".into_any_element()
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1183,6 +1183,7 @@ impl ExtensionStore {
|
||||||
command: crate::wit::SlashCommand {
|
command: crate::wit::SlashCommand {
|
||||||
name: slash_command_name.to_string(),
|
name: slash_command_name.to_string(),
|
||||||
description: slash_command.description.to_string(),
|
description: slash_command.description.to_string(),
|
||||||
|
tooltip_text: slash_command.tooltip_text.to_string(),
|
||||||
requires_argument: slash_command.requires_argument,
|
requires_argument: slash_command.requires_argument,
|
||||||
},
|
},
|
||||||
extension: wasm_extension.clone(),
|
extension: wasm_extension.clone(),
|
||||||
|
|
|
@ -5,6 +5,8 @@ interface slash-command {
|
||||||
name: string,
|
name: string,
|
||||||
/// The description of the slash command.
|
/// The description of the slash command.
|
||||||
description: string,
|
description: string,
|
||||||
|
/// The tooltip text to display for the run button.
|
||||||
|
tooltip-text: string,
|
||||||
/// Whether this slash command requires an argument.
|
/// Whether this slash command requires an argument.
|
||||||
requires-argument: bool,
|
requires-argument: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,6 +291,8 @@ impl Interactivity {
|
||||||
let action = action.downcast_ref().unwrap();
|
let action = action.downcast_ref().unwrap();
|
||||||
if phase == DispatchPhase::Capture {
|
if phase == DispatchPhase::Capture {
|
||||||
(listener)(action, cx)
|
(listener)(action, cx)
|
||||||
|
} else {
|
||||||
|
cx.propagate();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
|
@ -36,7 +36,7 @@ use git::{blame::Blame, repository::GitRepository};
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
|
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 itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -407,7 +407,7 @@ pub struct InlayHint {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A completion provided by a language server
|
/// A completion provided by a language server
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone)]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
/// The range of the buffer that will be replaced.
|
/// The range of the buffer that will be replaced.
|
||||||
pub old_range: Range<Anchor>,
|
pub old_range: Range<Anchor>,
|
||||||
|
@ -421,6 +421,21 @@ pub struct Completion {
|
||||||
pub documentation: Option<Documentation>,
|
pub documentation: Option<Documentation>,
|
||||||
/// The raw completion provided by the language server.
|
/// The raw completion provided by the language server.
|
||||||
pub lsp_completion: lsp::CompletionItem,
|
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
|
/// 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(
|
pub fn open_local_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
|
@ -9212,6 +9251,7 @@ impl Project {
|
||||||
runs: Default::default(),
|
runs: Default::default(),
|
||||||
filter_range: Default::default(),
|
filter_range: Default::default(),
|
||||||
},
|
},
|
||||||
|
confirm: None,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
|
@ -10883,6 +10923,7 @@ async fn populate_labels_for_completions(
|
||||||
server_id: completion.server_id,
|
server_id: completion.server_id,
|
||||||
documentation,
|
documentation,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
|
confirm: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,6 +184,7 @@ pub enum IconName {
|
||||||
Tab,
|
Tab,
|
||||||
Terminal,
|
Terminal,
|
||||||
Trash,
|
Trash,
|
||||||
|
TriangleRight,
|
||||||
Update,
|
Update,
|
||||||
WholeWord,
|
WholeWord,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
@ -303,6 +304,7 @@ impl IconName {
|
||||||
IconName::Tab => "icons/tab.svg",
|
IconName::Tab => "icons/tab.svg",
|
||||||
IconName::Terminal => "icons/terminal.svg",
|
IconName::Terminal => "icons/terminal.svg",
|
||||||
IconName::Trash => "icons/trash.svg",
|
IconName::Trash => "icons/trash.svg",
|
||||||
|
IconName::TriangleRight => "icons/triangle_right.svg",
|
||||||
IconName::Update => "icons/update.svg",
|
IconName::Update => "icons/update.svg",
|
||||||
IconName::WholeWord => "icons/word_search.svg",
|
IconName::WholeWord => "icons/word_search.svg",
|
||||||
IconName::XCircle => "icons/error.svg",
|
IconName::XCircle => "icons/error.svg",
|
||||||
|
|
|
@ -17,3 +17,4 @@ commit = "8432ffe32ccd360534837256747beb5b1c82fca1"
|
||||||
[slash_commands.gleam-project]
|
[slash_commands.gleam-project]
|
||||||
description = "Returns information about the current Gleam project."
|
description = "Returns information about the current Gleam project."
|
||||||
requires_argument = false
|
requires_argument = false
|
||||||
|
tooltip_text = "Insert Gleam project data"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue