mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Add slash commands for adding context into the assistant (#12102)
Tasks * [x] remove old flaps and output when editing a slash command * [x] the completing a command name that takes args, insert a space to prepare for typing an arg * [x] always trigger completions when typing in a slash command * [x] don't show line numbers * [x] implement `prompt` command * [x] `current-file` command * [x] state gets corrupted on `duplicate line up` on a slash command * [x] exclude slash command source from completion request Next steps: * show output token count in flap trailer * add `/project` command that matches project ambient context * delete ambient context Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
parent
d6e59bfae1
commit
a73a3ef243
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -348,6 +348,7 @@ dependencies = [
|
|||||||
"file_icons",
|
"file_icons",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"http 0.1.0",
|
"http 0.1.0",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
@ -21,6 +21,7 @@ editor.workspace = true
|
|||||||
file_icons.workspace = true
|
file_icons.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
|
@ -34,10 +34,12 @@ impl Default for CurrentProjectContext {
|
|||||||
impl CurrentProjectContext {
|
impl CurrentProjectContext {
|
||||||
/// Returns the [`CurrentProjectContext`] as a message to the language model.
|
/// Returns the [`CurrentProjectContext`] as a message to the language model.
|
||||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||||
self.enabled.then(|| LanguageModelRequestMessage {
|
self.enabled
|
||||||
role: Role::System,
|
.then(|| LanguageModelRequestMessage {
|
||||||
content: self.message.clone(),
|
role: Role::System,
|
||||||
})
|
content: self.message.clone(),
|
||||||
|
})
|
||||||
|
.filter(|message| !message.content.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
|
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
|
||||||
|
@ -87,10 +87,12 @@ impl RecentBuffersContext {
|
|||||||
|
|
||||||
/// Returns the [`RecentBuffersContext`] as a message to the language model.
|
/// Returns the [`RecentBuffersContext`] as a message to the language model.
|
||||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||||
self.enabled.then(|| LanguageModelRequestMessage {
|
self.enabled
|
||||||
role: Role::System,
|
.then(|| LanguageModelRequestMessage {
|
||||||
content: self.snapshot.message.to_string(),
|
role: Role::System,
|
||||||
})
|
content: self.snapshot.message.to_string(),
|
||||||
|
})
|
||||||
|
.filter(|message| !message.content.is_empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ mod prompt_library;
|
|||||||
mod prompts;
|
mod prompts;
|
||||||
mod saved_conversation;
|
mod saved_conversation;
|
||||||
mod search;
|
mod search;
|
||||||
|
mod slash_command;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
|
|
||||||
use ambient_context::AmbientContextSnapshot;
|
use ambient_context::AmbientContextSnapshot;
|
||||||
@ -16,6 +17,7 @@ use client::{proto, Client};
|
|||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
pub(crate) use completion_provider::*;
|
pub(crate) use completion_provider::*;
|
||||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||||
|
pub(crate) use prompt_library::*;
|
||||||
pub(crate) use saved_conversation::*;
|
pub(crate) use saved_conversation::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -233,7 +233,7 @@ impl CompletionProvider {
|
|||||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
CompletionProvider::Fake(_) => unimplemented!(),
|
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,10 +156,10 @@ impl PromptLibrary {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub struct UserPrompt {
|
pub struct UserPrompt {
|
||||||
version: String,
|
version: String,
|
||||||
title: String,
|
pub title: String,
|
||||||
author: String,
|
author: String,
|
||||||
languages: Vec<String>,
|
languages: Vec<String>,
|
||||||
prompt: String,
|
pub prompt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserPrompt {
|
impl UserPrompt {
|
||||||
|
319
crates/assistant/src/slash_command.rs
Normal file
319
crates/assistant/src/slash_command.rs
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::{CompletionProvider, Editor};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
|
use gpui::{AppContext, Model, Task, ViewContext, WindowHandle};
|
||||||
|
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use project::Project;
|
||||||
|
use rope::Point;
|
||||||
|
use std::{
|
||||||
|
ops::Range,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::PromptLibrary;
|
||||||
|
|
||||||
|
mod current_file_command;
|
||||||
|
mod file_command;
|
||||||
|
mod prompt_command;
|
||||||
|
|
||||||
|
pub(crate) struct SlashCommandCompletionProvider {
|
||||||
|
commands: Arc<SlashCommandRegistry>,
|
||||||
|
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct SlashCommandRegistry {
|
||||||
|
commands: HashMap<String, Box<dyn SlashCommand>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait SlashCommand: 'static + Send + Sync {
|
||||||
|
fn name(&self) -> String;
|
||||||
|
fn description(&self) -> String;
|
||||||
|
fn complete_argument(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
cancel: Arc<AtomicBool>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<Vec<String>>>;
|
||||||
|
fn requires_argument(&self) -> bool;
|
||||||
|
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SlashCommandInvocation {
|
||||||
|
pub output: Task<Result<String>>,
|
||||||
|
pub invalidated: oneshot::Receiver<()>,
|
||||||
|
pub cleanup: SlashCommandCleanup,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) 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(crate) struct SlashCommandLine {
|
||||||
|
/// The range within the line containing the command name.
|
||||||
|
pub name: Range<usize>,
|
||||||
|
/// The range within the line containing the command argument.
|
||||||
|
pub argument: Option<Range<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashCommandRegistry {
|
||||||
|
pub fn new(
|
||||||
|
project: Model<Project>,
|
||||||
|
prompt_library: Arc<PromptLibrary>,
|
||||||
|
window: Option<WindowHandle<Workspace>>,
|
||||||
|
) -> Arc<Self> {
|
||||||
|
let mut this = Self {
|
||||||
|
commands: HashMap::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.register_command(file_command::FileSlashCommand::new(project));
|
||||||
|
this.register_command(prompt_command::PromptSlashCommand::new(prompt_library));
|
||||||
|
if let Some(window) = window {
|
||||||
|
this.register_command(current_file_command::CurrentFileSlashCommand::new(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_command(&mut self, command: impl SlashCommand) {
|
||||||
|
self.commands.insert(command.name(), Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_names(&self) -> impl Iterator<Item = &String> {
|
||||||
|
self.commands.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn command(&self, name: &str) -> Option<&dyn SlashCommand> {
|
||||||
|
self.commands.get(name).map(|b| &**b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashCommandCompletionProvider {
|
||||||
|
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
|
||||||
|
Self {
|
||||||
|
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||||
|
commands,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_command_name(
|
||||||
|
&self,
|
||||||
|
command_name: &str,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
|
let candidates = self
|
||||||
|
.commands
|
||||||
|
.command_names()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, def)| StringMatchCandidate {
|
||||||
|
id: ix,
|
||||||
|
string: def.clone(),
|
||||||
|
char_bag: def.as_str().into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let commands = self.commands.clone();
|
||||||
|
let command_name = command_name.to_string();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
executor.clone().spawn(async move {
|
||||||
|
let matches = match_strings(
|
||||||
|
&candidates,
|
||||||
|
&command_name,
|
||||||
|
true,
|
||||||
|
usize::MAX,
|
||||||
|
&Default::default(),
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(matches
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mat| {
|
||||||
|
let command = commands.command(&mat.string)?;
|
||||||
|
let mut new_text = mat.string.clone();
|
||||||
|
if command.requires_argument() {
|
||||||
|
new_text.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(project::Completion {
|
||||||
|
old_range: range.clone(),
|
||||||
|
documentation: Some(Documentation::SingleLine(command.description())),
|
||||||
|
new_text,
|
||||||
|
label: CodeLabel::plain(mat.string, None),
|
||||||
|
server_id: LanguageServerId(0),
|
||||||
|
lsp_completion: Default::default(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_command_argument(
|
||||||
|
&self,
|
||||||
|
command_name: &str,
|
||||||
|
argument: String,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<Vec<project::Completion>>> {
|
||||||
|
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
let mut flag = self.cancel_flag.lock();
|
||||||
|
flag.store(true, SeqCst);
|
||||||
|
*flag = new_cancel_flag.clone();
|
||||||
|
|
||||||
|
if let Some(command) = self.commands.command(command_name) {
|
||||||
|
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
Ok(completions
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|arg| project::Completion {
|
||||||
|
old_range: range.clone(),
|
||||||
|
label: CodeLabel::plain(arg.clone(), None),
|
||||||
|
new_text: arg.clone(),
|
||||||
|
documentation: None,
|
||||||
|
server_id: LanguageServerId(0),
|
||||||
|
lsp_completion: Default::default(),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move { Ok(Vec::new()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionProvider for SlashCommandCompletionProvider {
|
||||||
|
fn completions(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
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 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))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
_: Model<Buffer>,
|
||||||
|
_: Vec<usize>,
|
||||||
|
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||||
|
_: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
Task::ready(Ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_additional_edits_for_completion(
|
||||||
|
&self,
|
||||||
|
_: Model<Buffer>,
|
||||||
|
_: project::Completion,
|
||||||
|
_: bool,
|
||||||
|
_: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Option<language::Transaction>>> {
|
||||||
|
Task::ready(Ok(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: language::Anchor,
|
||||||
|
_text: &str,
|
||||||
|
_trigger_in_words: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> bool {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let position = position.to_point(buffer);
|
||||||
|
let line_start = Point::new(position.row, 0);
|
||||||
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||||
|
if let Some(line) = lines.next() {
|
||||||
|
SlashCommandLine::parse(line).is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashCommandLine {
|
||||||
|
pub(crate) fn parse(line: &str) -> Option<Self> {
|
||||||
|
let mut call: Option<Self> = None;
|
||||||
|
let mut ix = 0;
|
||||||
|
for c in line.chars() {
|
||||||
|
let next_ix = ix + c.len_utf8();
|
||||||
|
if let Some(call) = &mut call {
|
||||||
|
// The command arguments start at the first non-whitespace character
|
||||||
|
// after the command name, and continue until the end of the line.
|
||||||
|
if let Some(argument) = &mut call.argument {
|
||||||
|
if (*argument).is_empty() && c.is_whitespace() {
|
||||||
|
argument.start = next_ix;
|
||||||
|
}
|
||||||
|
argument.end = next_ix;
|
||||||
|
}
|
||||||
|
// The command name ends at the first whitespace character.
|
||||||
|
else if !call.name.is_empty() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
call.argument = Some(next_ix..next_ix);
|
||||||
|
} else {
|
||||||
|
call.name.end = next_ix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The command name must begin with a letter.
|
||||||
|
else if c.is_alphabetic() {
|
||||||
|
call.name.end = next_ix;
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Commands start with a slash.
|
||||||
|
else if c == '/' {
|
||||||
|
call = Some(SlashCommandLine {
|
||||||
|
name: next_ix..next_ix,
|
||||||
|
argument: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// The line can't contain anything before the slash except for whitespace.
|
||||||
|
else if !c.is_whitespace() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
ix = next_ix;
|
||||||
|
}
|
||||||
|
call
|
||||||
|
}
|
||||||
|
}
|
135
crates/assistant/src/slash_command/current_file_command.rs
Normal file
135
crates/assistant/src/slash_command/current_file_command.rs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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 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, _argument: Option<&str>, 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
crates/assistant/src/slash_command/file_command.rs
Normal file
145
crates/assistant/src/slash_command/file_command.rs
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||||
|
use anyhow::Result;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use fuzzy::PathMatch;
|
||||||
|
use gpui::{AppContext, Model, Task};
|
||||||
|
use project::{PathMatchCandidateSet, Project};
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct FileSlashCommand {
|
||||||
|
project: Model<Project>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSlashCommand {
|
||||||
|
pub fn new(project: Model<Project>) -> Self {
|
||||||
|
Self { project }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_paths(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Vec<PathMatch>> {
|
||||||
|
let worktrees = self
|
||||||
|
.project
|
||||||
|
.read(cx)
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let include_root_name = worktrees.len() > 1;
|
||||||
|
let candidate_sets = worktrees
|
||||||
|
.into_iter()
|
||||||
|
.map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
PathMatchCandidateSet {
|
||||||
|
snapshot: worktree.snapshot(),
|
||||||
|
include_ignored: worktree
|
||||||
|
.root_entry()
|
||||||
|
.map_or(false, |entry| entry.is_ignored),
|
||||||
|
include_root_name,
|
||||||
|
directories_only: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
|
fuzzy::match_path_sets(
|
||||||
|
candidate_sets.as_slice(),
|
||||||
|
query.as_str(),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&cancellation_flag,
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashCommand for FileSlashCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"file".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"insert an entire file".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_argument(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_argument(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> gpui::Task<Result<Vec<String>>> {
|
||||||
|
let paths = self.search_paths(query, cancellation_flag, cx);
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
Ok(paths
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|path_match| {
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
path_match.path_prefix,
|
||||||
|
path_match.path.to_string_lossy()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = Path::new(argument);
|
||||||
|
let abs_path = project.worktrees().find_map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
worktree.entry_for_path(path)?;
|
||||||
|
worktree.absolutize(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(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let fs = project.fs().clone();
|
||||||
|
let argument = argument.to_string();
|
||||||
|
let output = 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("```");
|
||||||
|
output.push_str(&argument);
|
||||||
|
output.push('\n');
|
||||||
|
output.push_str(&content);
|
||||||
|
if !output.ends_with('\n') {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
output.push_str("```");
|
||||||
|
Ok(output)
|
||||||
|
});
|
||||||
|
SlashCommandInvocation {
|
||||||
|
output,
|
||||||
|
invalidated: oneshot::channel().1,
|
||||||
|
cleanup: SlashCommandCleanup::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
crates/assistant/src/slash_command/prompt_command.rs
Normal file
88
crates/assistant/src/slash_command/prompt_command.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
|
||||||
|
use crate::PromptLibrary;
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use fuzzy::StringMatchCandidate;
|
||||||
|
use gpui::{AppContext, Task};
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
|
||||||
|
pub(crate) struct PromptSlashCommand {
|
||||||
|
library: Arc<PromptLibrary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptSlashCommand {
|
||||||
|
pub fn new(library: Arc<PromptLibrary>) -> Self {
|
||||||
|
Self { library }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlashCommand for PromptSlashCommand {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
"prompt".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
"insert a prompt from the library".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_argument(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_argument(
|
||||||
|
&self,
|
||||||
|
query: String,
|
||||||
|
cancellation_flag: Arc<AtomicBool>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<Vec<String>>> {
|
||||||
|
let library = self.library.clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let candidates = library
|
||||||
|
.prompts()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.title))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&cancellation_flag,
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Ok(matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| candidates[mat.candidate_id].string.clone())
|
||||||
|
.collect())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, title: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
|
||||||
|
let Some(title) = title else {
|
||||||
|
return SlashCommandInvocation {
|
||||||
|
output: Task::ready(Err(anyhow!("missing prompt name"))),
|
||||||
|
invalidated: oneshot::channel().1,
|
||||||
|
cleanup: SlashCommandCleanup::default(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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.title == title)
|
||||||
|
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||||
|
Ok(prompt.prompt)
|
||||||
|
});
|
||||||
|
SlashCommandInvocation {
|
||||||
|
output,
|
||||||
|
invalidated: oneshot::channel().1,
|
||||||
|
cleanup: SlashCommandCleanup::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,17 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
|||||||
) -> Task<Result<Option<language::Transaction>>> {
|
) -> Task<Result<Option<language::Transaction>>> {
|
||||||
Task::ready(Ok(None))
|
Task::ready(Ok(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
_buffer: &Model<Buffer>,
|
||||||
|
_position: language::Anchor,
|
||||||
|
text: &str,
|
||||||
|
_trigger_in_words: bool,
|
||||||
|
_cx: &mut ViewContext<Editor>,
|
||||||
|
) -> bool {
|
||||||
|
text == "@"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageEditor {
|
impl MessageEditor {
|
||||||
|
@ -449,6 +449,9 @@ pub struct Editor {
|
|||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
show_breadcrumbs: bool,
|
show_breadcrumbs: bool,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
|
show_line_numbers: Option<bool>,
|
||||||
|
show_git_diff_gutter: Option<bool>,
|
||||||
|
show_code_actions: Option<bool>,
|
||||||
show_wrap_guides: Option<bool>,
|
show_wrap_guides: Option<bool>,
|
||||||
placeholder_text: Option<Arc<str>>,
|
placeholder_text: Option<Arc<str>>,
|
||||||
highlight_order: usize,
|
highlight_order: usize,
|
||||||
@ -517,6 +520,9 @@ pub struct Editor {
|
|||||||
pub struct EditorSnapshot {
|
pub struct EditorSnapshot {
|
||||||
pub mode: EditorMode,
|
pub mode: EditorMode,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
|
show_line_numbers: Option<bool>,
|
||||||
|
show_git_diff_gutter: Option<bool>,
|
||||||
|
show_code_actions: Option<bool>,
|
||||||
render_git_blame_gutter: bool,
|
render_git_blame_gutter: bool,
|
||||||
pub display_snapshot: DisplaySnapshot,
|
pub display_snapshot: DisplaySnapshot,
|
||||||
pub placeholder_text: Option<Arc<str>>,
|
pub placeholder_text: Option<Arc<str>>,
|
||||||
@ -1646,6 +1652,9 @@ impl Editor {
|
|||||||
mode,
|
mode,
|
||||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||||
show_gutter: mode == EditorMode::Full,
|
show_gutter: mode == EditorMode::Full,
|
||||||
|
show_line_numbers: None,
|
||||||
|
show_git_diff_gutter: None,
|
||||||
|
show_code_actions: None,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
highlight_order: 0,
|
highlight_order: 0,
|
||||||
@ -1881,6 +1890,9 @@ impl Editor {
|
|||||||
EditorSnapshot {
|
EditorSnapshot {
|
||||||
mode: self.mode,
|
mode: self.mode,
|
||||||
show_gutter: self.show_gutter,
|
show_gutter: self.show_gutter,
|
||||||
|
show_line_numbers: self.show_line_numbers,
|
||||||
|
show_git_diff_gutter: self.show_git_diff_gutter,
|
||||||
|
show_code_actions: self.show_code_actions,
|
||||||
render_git_blame_gutter: self.render_git_blame_gutter(cx),
|
render_git_blame_gutter: self.render_git_blame_gutter(cx),
|
||||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||||
scroll_anchor: self.scroll_manager.anchor(),
|
scroll_anchor: self.scroll_manager.anchor(),
|
||||||
@ -1933,8 +1945,8 @@ impl Editor {
|
|||||||
self.custom_context_menu = Some(Box::new(f))
|
self.custom_context_menu = Some(Box::new(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
|
pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
|
||||||
self.completion_provider = Some(hub);
|
self.completion_provider = Some(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_inline_completion_provider<T>(
|
pub fn set_inline_completion_provider<T>(
|
||||||
@ -3280,22 +3292,41 @@ impl Editor {
|
|||||||
trigger_in_words: bool,
|
trigger_in_words: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if !EditorSettings::get_global(cx).show_completions_on_input {
|
if self.is_completion_trigger(text, trigger_in_words, cx) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selection = self.selections.newest_anchor();
|
|
||||||
if self
|
|
||||||
.buffer
|
|
||||||
.read(cx)
|
|
||||||
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
|
|
||||||
{
|
|
||||||
self.show_completions(&ShowCompletions, cx);
|
self.show_completions(&ShowCompletions, cx);
|
||||||
} else {
|
} else {
|
||||||
self.hide_context_menu(cx);
|
self.hide_context_menu(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
trigger_in_words: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
let position = self.selections.newest_anchor().head();
|
||||||
|
let multibuffer = self.buffer.read(cx);
|
||||||
|
let Some(buffer) = position
|
||||||
|
.buffer_id
|
||||||
|
.and_then(|buffer_id| multibuffer.buffer(buffer_id).clone())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(completion_provider) = &self.completion_provider {
|
||||||
|
completion_provider.is_completion_trigger(
|
||||||
|
&buffer,
|
||||||
|
position.text_anchor,
|
||||||
|
text,
|
||||||
|
trigger_in_words,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If any empty selections is touching the start of its innermost containing autoclose
|
/// If any empty selections is touching the start of its innermost containing autoclose
|
||||||
/// region, expand it to select the brackets.
|
/// region, expand it to select the brackets.
|
||||||
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
|
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
@ -9613,8 +9644,27 @@ impl Editor {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
|
||||||
self.show_wrap_guides = Some(show_gutter);
|
self.show_line_numbers = Some(show_line_numbers);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_git_diff_gutter(
|
||||||
|
&mut self,
|
||||||
|
show_git_diff_gutter: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.show_git_diff_gutter = Some(show_git_diff_gutter);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.show_code_actions = Some(show_code_actions);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.show_wrap_guides = Some(show_wrap_guides);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10888,6 +10938,15 @@ pub trait CompletionProvider {
|
|||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> Task<Result<Option<language::Transaction>>>;
|
) -> Task<Result<Option<language::Transaction>>>;
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: language::Anchor,
|
||||||
|
text: &str,
|
||||||
|
trigger_in_words: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionProvider for Model<Project> {
|
impl CompletionProvider for Model<Project> {
|
||||||
@ -10925,6 +10984,40 @@ impl CompletionProvider for Model<Project> {
|
|||||||
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_completion_trigger(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: language::Anchor,
|
||||||
|
text: &str,
|
||||||
|
trigger_in_words: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> bool {
|
||||||
|
if !EditorSettings::get_global(cx).show_completions_on_input {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chars = text.chars();
|
||||||
|
let char = if let Some(char) = chars.next() {
|
||||||
|
char
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if chars.next().is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let scope = buffer.snapshot().language_scope_at(position);
|
||||||
|
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
.completion_triggers()
|
||||||
|
.iter()
|
||||||
|
.any(|string| string == text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inlay_hint_settings(
|
fn inlay_hint_settings(
|
||||||
@ -11030,13 +11123,17 @@ impl EditorSnapshot {
|
|||||||
}
|
}
|
||||||
let descent = cx.text_system().descent(font_id, font_size);
|
let descent = cx.text_system().descent(font_id, font_size);
|
||||||
|
|
||||||
let show_git_gutter = matches!(
|
let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
|
||||||
ProjectSettings::get_global(cx).git.git_gutter,
|
matches!(
|
||||||
Some(GitGutterSetting::TrackedFiles)
|
ProjectSettings::get_global(cx).git.git_gutter,
|
||||||
);
|
Some(GitGutterSetting::TrackedFiles)
|
||||||
|
)
|
||||||
|
});
|
||||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||||
let gutter_lines_enabled = gutter_settings.line_numbers;
|
let show_line_numbers = self
|
||||||
let line_gutter_width = if gutter_lines_enabled {
|
.show_line_numbers
|
||||||
|
.unwrap_or_else(|| gutter_settings.line_numbers);
|
||||||
|
let line_gutter_width = if show_line_numbers {
|
||||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||||
let min_width_for_number_on_gutter = em_width * 4.0;
|
let min_width_for_number_on_gutter = em_width * 4.0;
|
||||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||||
@ -11044,26 +11141,30 @@ impl EditorSnapshot {
|
|||||||
0.0.into()
|
0.0.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let show_code_actions = self
|
||||||
|
.show_code_actions
|
||||||
|
.unwrap_or_else(|| gutter_settings.code_actions);
|
||||||
|
|
||||||
let git_blame_entries_width = self
|
let git_blame_entries_width = self
|
||||||
.render_git_blame_gutter
|
.render_git_blame_gutter
|
||||||
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
|
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
|
||||||
|
|
||||||
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
|
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
|
||||||
left_padding += if gutter_settings.code_actions {
|
left_padding += if show_code_actions {
|
||||||
em_width * 3.0
|
em_width * 3.0
|
||||||
} else if show_git_gutter && gutter_lines_enabled {
|
} else if show_git_gutter && show_line_numbers {
|
||||||
em_width * 2.0
|
em_width * 2.0
|
||||||
} else if show_git_gutter || gutter_lines_enabled {
|
} else if show_git_gutter || show_line_numbers {
|
||||||
em_width
|
em_width
|
||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
|
|
||||||
let right_padding = if gutter_settings.folds && gutter_lines_enabled {
|
let right_padding = if gutter_settings.folds && show_line_numbers {
|
||||||
em_width * 4.0
|
em_width * 4.0
|
||||||
} else if gutter_settings.folds {
|
} else if gutter_settings.folds {
|
||||||
em_width * 3.0
|
em_width * 3.0
|
||||||
} else if gutter_lines_enabled {
|
} else if show_line_numbers {
|
||||||
em_width
|
em_width
|
||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
|
@ -1623,6 +1623,13 @@ impl EditorElement {
|
|||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Vec<Option<ShapedLine>> {
|
) -> Vec<Option<ShapedLine>> {
|
||||||
|
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
|
||||||
|
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
|
||||||
|
});
|
||||||
|
if !include_line_numbers {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
let editor = self.editor.read(cx);
|
let editor = self.editor.read(cx);
|
||||||
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
|
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
|
||||||
let newest = editor.selections.newest::<Point>(cx);
|
let newest = editor.selections.newest::<Point>(cx);
|
||||||
@ -1638,54 +1645,47 @@ impl EditorElement {
|
|||||||
.head
|
.head
|
||||||
});
|
});
|
||||||
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
||||||
let include_line_numbers =
|
|
||||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
|
|
||||||
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
|
|
||||||
let mut line_number = String::new();
|
|
||||||
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
|
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
|
||||||
let relative_to = if is_relative {
|
let relative_to = if is_relative {
|
||||||
Some(newest_selection_head.row())
|
Some(newest_selection_head.row())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
|
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
|
||||||
|
let mut line_number = String::new();
|
||||||
for (ix, row) in buffer_rows.into_iter().enumerate() {
|
buffer_rows
|
||||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
.into_iter()
|
||||||
let color = if active_rows.contains_key(&display_row) {
|
.enumerate()
|
||||||
cx.theme().colors().editor_active_line_number
|
.map(|(ix, multibuffer_row)| {
|
||||||
} else {
|
let multibuffer_row = multibuffer_row?;
|
||||||
cx.theme().colors().editor_line_number
|
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||||
};
|
let color = if active_rows.contains_key(&display_row) {
|
||||||
if let Some(multibuffer_row) = row {
|
cx.theme().colors().editor_active_line_number
|
||||||
if include_line_numbers {
|
} else {
|
||||||
line_number.clear();
|
cx.theme().colors().editor_line_number
|
||||||
let default_number = multibuffer_row.0 + 1;
|
};
|
||||||
let number = relative_rows
|
line_number.clear();
|
||||||
.get(&DisplayRow(ix as u32 + rows.start.0))
|
let default_number = multibuffer_row.0 + 1;
|
||||||
.unwrap_or(&default_number);
|
let number = relative_rows
|
||||||
write!(&mut line_number, "{number}").unwrap();
|
.get(&DisplayRow(ix as u32 + rows.start.0))
|
||||||
let run = TextRun {
|
.unwrap_or(&default_number);
|
||||||
len: line_number.len(),
|
write!(&mut line_number, "{number}").unwrap();
|
||||||
font: self.style.text.font(),
|
let run = TextRun {
|
||||||
color,
|
len: line_number.len(),
|
||||||
background_color: None,
|
font: self.style.text.font(),
|
||||||
underline: None,
|
color,
|
||||||
strikethrough: None,
|
background_color: None,
|
||||||
};
|
underline: None,
|
||||||
let shaped_line = cx
|
strikethrough: None,
|
||||||
.text_system()
|
};
|
||||||
.shape_line(line_number.clone().into(), font_size, &[run])
|
let shaped_line = cx
|
||||||
.unwrap();
|
.text_system()
|
||||||
shaped_line_numbers.push(Some(shaped_line));
|
.shape_line(line_number.clone().into(), font_size, &[run])
|
||||||
}
|
.unwrap();
|
||||||
} else {
|
Some(shaped_line)
|
||||||
shaped_line_numbers.push(None);
|
})
|
||||||
}
|
.collect()
|
||||||
}
|
|
||||||
|
|
||||||
shaped_line_numbers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout_gutter_fold_toggles(
|
fn layout_gutter_fold_toggles(
|
||||||
@ -2513,10 +2513,16 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let show_git_gutter = matches!(
|
let show_git_gutter = layout
|
||||||
ProjectSettings::get_global(cx).git.git_gutter,
|
.position_map
|
||||||
Some(GitGutterSetting::TrackedFiles)
|
.snapshot
|
||||||
);
|
.show_git_diff_gutter
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
matches!(
|
||||||
|
ProjectSettings::get_global(cx).git.git_gutter,
|
||||||
|
Some(GitGutterSetting::TrackedFiles)
|
||||||
|
)
|
||||||
|
});
|
||||||
if show_git_gutter {
|
if show_git_gutter {
|
||||||
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
|
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
|
||||||
}
|
}
|
||||||
@ -4281,7 +4287,11 @@ impl Element for EditorElement {
|
|||||||
gutter_dimensions.width - gutter_dimensions.left_padding,
|
gutter_dimensions.width - gutter_dimensions.left_padding,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
if gutter_settings.code_actions {
|
|
||||||
|
let show_code_actions = snapshot
|
||||||
|
.show_code_actions
|
||||||
|
.unwrap_or_else(|| gutter_settings.code_actions);
|
||||||
|
if show_code_actions {
|
||||||
let newest_selection_point =
|
let newest_selection_point =
|
||||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||||
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
|
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
|
||||||
|
@ -4443,6 +4443,9 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe impl<V> Send for WindowHandle<V> {}
|
||||||
|
unsafe impl<V> Sync for WindowHandle<V> {}
|
||||||
|
|
||||||
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct AnyWindowHandle {
|
pub struct AnyWindowHandle {
|
||||||
|
@ -110,6 +110,7 @@ impl MultiBufferRow {
|
|||||||
pub const MIN: Self = Self(0);
|
pub const MIN: Self = Self(0);
|
||||||
pub const MAX: Self = Self(u32::MAX);
|
pub const MAX: Self = Self(u32::MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct History {
|
struct History {
|
||||||
next_transaction_id: TransactionId,
|
next_transaction_id: TransactionId,
|
||||||
@ -1531,46 +1532,6 @@ impl MultiBuffer {
|
|||||||
.map(|state| state.buffer.clone())
|
.map(|state| state.buffer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completion_trigger(
|
|
||||||
&self,
|
|
||||||
position: Anchor,
|
|
||||||
text: &str,
|
|
||||||
trigger_in_words: bool,
|
|
||||||
cx: &AppContext,
|
|
||||||
) -> bool {
|
|
||||||
let mut chars = text.chars();
|
|
||||||
let char = if let Some(char) = chars.next() {
|
|
||||||
char
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if chars.next().is_some() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = self.snapshot(cx);
|
|
||||||
let position = position.to_offset(&snapshot);
|
|
||||||
let scope = snapshot.language_scope_at(position);
|
|
||||||
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let anchor = snapshot.anchor_before(position);
|
|
||||||
anchor
|
|
||||||
.buffer_id
|
|
||||||
.and_then(|buffer_id| {
|
|
||||||
let buffer = self.buffers.borrow().get(&buffer_id)?.buffer.clone();
|
|
||||||
Some(
|
|
||||||
buffer
|
|
||||||
.read(cx)
|
|
||||||
.completion_triggers()
|
|
||||||
.iter()
|
|
||||||
.any(|string| string == text),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn language_at<T: ToOffset>(&self, point: T, cx: &AppContext) -> Option<Arc<Language>> {
|
pub fn language_at<T: ToOffset>(&self, point: T, cx: &AppContext) -> Option<Arc<Language>> {
|
||||||
self.point_to_buffer_offset(point, cx)
|
self.point_to_buffer_offset(point, cx)
|
||||||
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
|
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
|
||||||
|
@ -2166,6 +2166,31 @@ impl BufferSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_edits_since_in_range(&self, since: &clock::Global, range: Range<Anchor>) -> bool {
|
||||||
|
if *since != self.version {
|
||||||
|
let start_fragment_id = self.fragment_id_for_anchor(&range.start);
|
||||||
|
let end_fragment_id = self.fragment_id_for_anchor(&range.end);
|
||||||
|
let mut cursor = self
|
||||||
|
.fragments
|
||||||
|
.filter::<_, usize>(move |summary| !since.observed_all(&summary.max_version));
|
||||||
|
cursor.next(&None);
|
||||||
|
while let Some(fragment) = cursor.item() {
|
||||||
|
if fragment.id > *end_fragment_id {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if fragment.id > *start_fragment_id {
|
||||||
|
let was_visible = fragment.was_visible(since, &self.undo_map);
|
||||||
|
let is_visible = fragment.visible;
|
||||||
|
if was_visible != is_visible {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor.next(&None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_edits_since(&self, since: &clock::Global) -> bool {
|
pub fn has_edits_since(&self, since: &clock::Global) -> bool {
|
||||||
if *since != self.version {
|
if *since != self.version {
|
||||||
let mut cursor = self
|
let mut cursor = self
|
||||||
|
Loading…
Reference in New Issue
Block a user