Compare commits
12 Commits
breakpoint
...
custom-too
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38fcadf948 | ||
|
|
ba767a1998 | ||
|
|
e5cbac1373 | ||
|
|
23c3f5f410 | ||
|
|
b3be294c90 | ||
|
|
af5318df98 | ||
|
|
60c420a2da | ||
|
|
ee6c33ffb3 | ||
|
|
9ae4f4b158 | ||
|
|
915a1cb116 | ||
|
|
aead0e11ff | ||
|
|
53375434cf |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -225,7 +225,7 @@ jobs:
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"label": "Debug Zed with LLDB",
|
||||
"adapter": "LLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed (GDB)",
|
||||
"label": "Debug Zed with GDB",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -7080,9 +7080,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.8.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.2",
|
||||
@@ -7937,9 +7937,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8610,9 +8610,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.45"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -18106,7 +18106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.182.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
@@ -856,8 +856,8 @@ impl ActiveThread {
|
||||
&tool_use.input,
|
||||
self.thread
|
||||
.read(cx)
|
||||
.tool_result(&tool_use.id)
|
||||
.map(|result| result.content.clone().into())
|
||||
.output_for_tool(&tool_use.id)
|
||||
.map(|output| output.clone().into())
|
||||
.unwrap_or("".into()),
|
||||
cx,
|
||||
);
|
||||
@@ -2202,18 +2202,23 @@ impl ActiveThread {
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(div().w_full().text_ui_sm(cx).children(
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
Some(card.clone().into_any_element())
|
||||
} else {
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
}),
|
||||
},
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::Running => container.child(
|
||||
@@ -2255,10 +2260,11 @@ impl ActiveThread {
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm(cx)
|
||||
.children(rendered_tool_use.as_ref().map(|rendered| {
|
||||
.child(div().text_ui_sm(cx).children(
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
Some(card.clone().into_any_element())
|
||||
} else {
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
@@ -2269,8 +2275,10 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
})),
|
||||
),
|
||||
.into_any_element()
|
||||
})
|
||||
},
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
@@ -2840,10 +2848,10 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let path = directory_context.project_path.clone();
|
||||
let project_path = directory_context.project_path(cx);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
if let Some(entry) = project.entry_for_path(&path, cx) {
|
||||
if let Some(entry) = project.entry_for_path(&project_path, cx) {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::ProjectPath;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
@@ -69,10 +69,21 @@ pub struct FileContext {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub id: ContextId,
|
||||
pub project_path: ProjectPath,
|
||||
pub worktree: Entity<Worktree>,
|
||||
pub path: Arc<Path>,
|
||||
/// Buffers of the files within the directory.
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn project_path(&self, cx: &App) -> ProjectPath {
|
||||
ProjectPath {
|
||||
worktree_id: self.worktree.read(cx).id(),
|
||||
path: self.path.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub id: ContextId,
|
||||
@@ -86,12 +97,11 @@ pub struct FetchedUrlContext {
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
// TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
|
||||
// a WeakEntity and handle removal from the UI when it has dropped.
|
||||
pub thread: Entity<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
@@ -105,12 +115,11 @@ impl ThreadContext {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
|
||||
// a WeakEntity and handle removal from the UI when it has dropped.
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub file: Arc<dyn File>,
|
||||
pub version: clock::Global,
|
||||
|
||||
@@ -289,12 +289,14 @@ impl ContextPicker {
|
||||
path_prefix,
|
||||
} => {
|
||||
let context_store = self.context_store.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let path = project_path.path.clone();
|
||||
|
||||
ContextMenuItem::custom_entry(
|
||||
move |_window, cx| {
|
||||
render_file_context_entry(
|
||||
ElementId::NamedInteger("ctx-recent".into(), ix),
|
||||
worktree_id,
|
||||
&path,
|
||||
&path_prefix,
|
||||
false,
|
||||
@@ -466,7 +468,7 @@ fn recent_context_picker_entries(
|
||||
recent.extend(
|
||||
workspace
|
||||
.recent_navigation_history_iter(cx)
|
||||
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||
.filter(|(path, _)| !current_files.contains(path))
|
||||
.take(4)
|
||||
.filter_map(|(project_path, _)| {
|
||||
project
|
||||
|
||||
@@ -189,6 +189,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.toggle_state(selected)
|
||||
.child(render_file_context_entry(
|
||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||
WorktreeId::from_usize(mat.worktree_id),
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
mat.is_dir,
|
||||
@@ -328,19 +329,26 @@ pub fn extract_file_name_and_directory(
|
||||
|
||||
pub fn render_file_context_entry(
|
||||
id: ElementId,
|
||||
path: &Path,
|
||||
worktree_id: WorktreeId,
|
||||
path: &Arc<Path>,
|
||||
path_prefix: &Arc<str>,
|
||||
is_directory: bool,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
|
||||
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
if is_directory {
|
||||
context_store.read(cx).includes_directory(path)
|
||||
context_store.read(cx).includes_directory(&project_path)
|
||||
} else {
|
||||
context_store.read(cx).will_include_file_path(path, cx)
|
||||
context_store
|
||||
.read(cx)
|
||||
.will_include_file_path(&project_path, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -380,8 +388,9 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
FileInclusion::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
FileInclusion::InDirectory(directory_project_path) => {
|
||||
// TODO: Consider using worktree full_path to include worktree name.
|
||||
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
|
||||
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -395,7 +404,7 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Included").size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(Tooltip::text(format!("in {dir_name}")))
|
||||
.tooltip(Tooltip::text(format!("in {directory_path}")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -28,7 +28,7 @@ pub struct ContextStore {
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
next_context_id: ContextId,
|
||||
files: BTreeMap<BufferId, ContextId>,
|
||||
directories: HashMap<PathBuf, ContextId>,
|
||||
directories: HashMap<ProjectPath, ContextId>,
|
||||
symbols: HashMap<ContextSymbolId, ContextId>,
|
||||
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
|
||||
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
|
||||
@@ -93,7 +93,7 @@ impl ContextStore {
|
||||
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
|
||||
|
||||
let already_included = this.update(cx, |this, cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
match this.will_include_buffer(buffer_id, &project_path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
this.remove_context(context_id, cx);
|
||||
@@ -159,7 +159,7 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let already_included = match self.includes_directory(&project_path.path) {
|
||||
let already_included = match self.includes_directory(&project_path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id, cx);
|
||||
@@ -223,14 +223,12 @@ impl ContextStore {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if context_buffers.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No text files found in {}",
|
||||
&project_path.path.display()
|
||||
));
|
||||
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
|
||||
return Err(anyhow!("No text files found in {}", &full_path.display()));
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_directory(project_path, context_buffers, cx);
|
||||
this.insert_directory(worktree, project_path, context_buffers, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -239,17 +237,20 @@ impl ContextStore {
|
||||
|
||||
fn insert_directory(
|
||||
&mut self,
|
||||
worktree: Entity<Worktree>,
|
||||
project_path: ProjectPath,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(project_path.path.to_path_buf(), id);
|
||||
let path = project_path.path.clone();
|
||||
self.directories.insert(project_path, id);
|
||||
|
||||
self.context
|
||||
.push(AssistantContext::Directory(DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
worktree,
|
||||
path,
|
||||
context_buffers,
|
||||
}));
|
||||
cx.notify();
|
||||
@@ -478,23 +479,31 @@ impl ContextStore {
|
||||
/// Returns whether the buffer is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory. Directory inclusion is based on paths rather than
|
||||
/// buffer IDs as the directory will be re-scanned.
|
||||
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
|
||||
pub fn will_include_buffer(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.files.get(&buffer_id) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
/// Returns whether this file path is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory.
|
||||
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
|
||||
pub fn will_include_file_path(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
cx: &App,
|
||||
) -> Option<FileInclusion> {
|
||||
if !self.files.is_empty() {
|
||||
let found_file_context = self.context.iter().find(|context| match &context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let buffer = file_context.context_buffer.buffer.read(cx);
|
||||
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
|
||||
*file_path == *path
|
||||
if let Some(context_path) = buffer.project_path(cx) {
|
||||
&context_path == project_path
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -506,31 +515,40 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
fn will_include_file_path_via_directory(
|
||||
&self,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<FileInclusion> {
|
||||
if self.directories.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buf = path.to_path_buf();
|
||||
let mut path_buf = project_path.path.to_path_buf();
|
||||
|
||||
while buf.pop() {
|
||||
if let Some(_) = self.directories.get(&buf) {
|
||||
return Some(FileInclusion::InDirectory(buf));
|
||||
while path_buf.pop() {
|
||||
// TODO: This isn't very efficient. Consider using a better representation of the
|
||||
// directories map.
|
||||
let directory_project_path = ProjectPath {
|
||||
worktree_id: project_path.worktree_id,
|
||||
path: path_buf.clone().into(),
|
||||
};
|
||||
if let Some(_) = self.directories.get(&directory_project_path) {
|
||||
return Some(FileInclusion::InDirectory(directory_project_path));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(path) {
|
||||
pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(project_path) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
self.will_include_file_path_via_directory(project_path)
|
||||
}
|
||||
|
||||
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
|
||||
@@ -564,13 +582,13 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
|
||||
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
|
||||
self.context
|
||||
.iter()
|
||||
.filter_map(|context| match context {
|
||||
AssistantContext::File(file) => {
|
||||
let buffer = file.context_buffer.buffer.read(cx);
|
||||
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
|
||||
buffer.project_path(cx)
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
@@ -587,7 +605,7 @@ impl ContextStore {
|
||||
|
||||
pub enum FileInclusion {
|
||||
Direct(ContextId),
|
||||
InDirectory(PathBuf),
|
||||
InDirectory(ProjectPath),
|
||||
}
|
||||
|
||||
// ContextBuffer without text.
|
||||
@@ -654,19 +672,6 @@ fn collect_buffer_info_and_text(
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
|
||||
if let Some(file) = buffer.file() {
|
||||
let mut path = file.path().clone();
|
||||
if path.as_os_str().is_empty() {
|
||||
path = file.full_path(cx).into();
|
||||
}
|
||||
Some(path)
|
||||
} else {
|
||||
log::error!("Buffer that had a path unexpectedly no longer has a path.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
let path_extension = path.extension().and_then(|ext| ext.to_str());
|
||||
let path_string = path.to_string_lossy();
|
||||
@@ -742,13 +747,13 @@ pub fn refresh_context_store_text(
|
||||
}
|
||||
}
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let directory_path = directory_context.project_path(cx);
|
||||
let should_refresh = changed_buffers.is_empty()
|
||||
|| changed_buffers.iter().any(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
|
||||
path.starts_with(&directory_context.project_path.path)
|
||||
})
|
||||
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
|
||||
return false;
|
||||
};
|
||||
buffer_path.starts_with(&directory_path)
|
||||
});
|
||||
|
||||
if should_refresh {
|
||||
@@ -835,14 +840,16 @@ fn refresh_directory_text(
|
||||
let context_buffers = future::join_all(futures);
|
||||
|
||||
let id = directory_context.id;
|
||||
let project_path = directory_context.project_path.clone();
|
||||
let worktree = directory_context.worktree.clone();
|
||||
let path = directory_context.path.clone();
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_buffers = context_buffers.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_directory_context = DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
worktree,
|
||||
path,
|
||||
context_buffers,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Directory(new_directory_context));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashSet;
|
||||
@@ -9,6 +10,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
@@ -93,26 +95,23 @@ impl ContextStrip {
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.full_path(cx);
|
||||
let project_path = active_buffer.project_path(cx)?;
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), &path)
|
||||
.will_include_buffer(active_buffer.remote_id(), &project_path)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
let file_name = active_buffer.file()?.file_name(cx);
|
||||
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
name: file_name.to_string_lossy().into_owned().into(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
icon_path,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::Arc;
|
||||
use agent_rules::load_worktree_rules_file;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use fs::Fs;
|
||||
@@ -604,8 +604,12 @@ impl Thread {
|
||||
self.tool_use.tool_results_for_message(id)
|
||||
}
|
||||
|
||||
pub fn tool_result(&self, id: &LanguageModelToolUseId) -> Option<&LanguageModelToolResult> {
|
||||
self.tool_use.tool_result(id)
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
Some(&self.tool_use.tool_result(id)?.content)
|
||||
}
|
||||
|
||||
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&gpui::AnyView> {
|
||||
self.tool_use.tool_result_card(id)
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
@@ -1450,8 +1454,11 @@ impl Thread {
|
||||
) -> Task<()> {
|
||||
let tool_name: Arc<str> = tool.name().into();
|
||||
|
||||
let run_tool = if self.tools.is_disabled(&tool.source(), &tool_name) {
|
||||
Task::ready(Err(anyhow!("tool is disabled: {tool_name}")))
|
||||
let tool_result = if self.tools.is_disabled(&tool.source(), &tool_name) {
|
||||
ToolResult {
|
||||
output: Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))),
|
||||
card: None,
|
||||
}
|
||||
} else {
|
||||
tool.run(
|
||||
input,
|
||||
@@ -1462,9 +1469,15 @@ impl Thread {
|
||||
)
|
||||
};
|
||||
|
||||
// Store the card separately if it exists
|
||||
if let Some(card) = tool_result.card.clone() {
|
||||
self.tool_use
|
||||
.insert_tool_result_card(tool_use_id.clone(), card);
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
async move |thread: WeakEntity<Thread>, cx| {
|
||||
let output = run_tool.await;
|
||||
let output = tool_result.output.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
|
||||
@@ -54,6 +54,7 @@ pub struct ToolUseState {
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, gpui::AnyView>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
@@ -66,6 +67,7 @@ impl ToolUseState {
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +259,18 @@ impl ToolUseState {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&gpui::AnyView> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: gpui::AnyView,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
|
||||
@@ -280,9 +280,10 @@ impl AddedContext {
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
|
||||
// handle renames?
|
||||
let full_path = &directory_context.project_path.path;
|
||||
let full_path = directory_context
|
||||
.worktree
|
||||
.read(cx)
|
||||
.full_path(&directory_context.path);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
|
||||
@@ -149,7 +149,7 @@ impl HeadlessAssistant {
|
||||
.entry(pending_tool_use.name.clone())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
|
||||
if let Some(tool_result) = thread.read(cx).output_for_tool(tool_use_id) {
|
||||
println!("Tool result: {:?}", tool_result);
|
||||
}
|
||||
if thread.read(cx).all_tools_finished() {
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use gpui::{AnyView, App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -22,6 +22,22 @@ pub fn init(cx: &mut App) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
/// and an optional card view that can be rendered immediately.
|
||||
pub struct ToolResult {
|
||||
/// The asynchronous task that will eventually resolve to the tool's output
|
||||
pub output: Task<Result<String>>,
|
||||
/// An optional view to present the output of the tool.
|
||||
pub card: Option<AnyView>,
|
||||
}
|
||||
|
||||
impl From<Task<Result<String>>> for ToolResult {
|
||||
/// Convert from a task to a ToolResult with no card
|
||||
fn from(output: Task<Result<String>>) -> Self {
|
||||
Self { output, card: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub enum ToolSource {
|
||||
/// A native tool built-in to Zed.
|
||||
@@ -66,7 +82,7 @@ pub trait Tool: 'static + Send + Sync {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
) -> ToolResult;
|
||||
}
|
||||
|
||||
impl Debug for dyn Tool {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
@@ -76,10 +76,10 @@ impl Tool for BashTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input: BashToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let project = project.read(cx);
|
||||
@@ -90,13 +90,15 @@ impl Tool for BashTool {
|
||||
|
||||
let only_worktree = match worktrees.next() {
|
||||
Some(worktree) => worktree,
|
||||
None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
|
||||
None => {
|
||||
return Task::ready(Err(anyhow!("No worktrees found in the project"))).into();
|
||||
}
|
||||
};
|
||||
|
||||
if worktrees.next().is_some() {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
|
||||
)));
|
||||
))).into();
|
||||
}
|
||||
|
||||
only_worktree.read(cx).abs_path()
|
||||
@@ -108,7 +110,8 @@ impl Tool for BashTool {
|
||||
{
|
||||
return Task::ready(Err(anyhow!(
|
||||
"The absolute path must be within one of the project's worktrees"
|
||||
)));
|
||||
)))
|
||||
.into();
|
||||
}
|
||||
|
||||
input_path.into()
|
||||
@@ -117,13 +120,15 @@ impl Tool for BashTool {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"`cd` directory {} not found in the project",
|
||||
&input.cd
|
||||
)));
|
||||
)))
|
||||
.into();
|
||||
};
|
||||
|
||||
worktree.read(cx).abs_path()
|
||||
};
|
||||
|
||||
cx.background_spawn(run_command_limited(working_dir, input.command))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
|
||||
use futures::future::join_all;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -219,14 +219,14 @@ impl Tool for BatchTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<BatchToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
if input.invocations.is_empty() {
|
||||
return Task::ready(Err(anyhow!("No tool invocations provided")));
|
||||
return Task::ready(Err(anyhow!("No tool invocations provided"))).into();
|
||||
}
|
||||
|
||||
let run_tools_concurrently = input.run_tools_concurrently;
|
||||
@@ -257,11 +257,11 @@ impl Tool for BatchTool {
|
||||
let project = project.clone();
|
||||
let action_log = action_log.clone();
|
||||
let messages = messages.clone();
|
||||
let task = cx
|
||||
let tool_result = cx
|
||||
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
|
||||
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
|
||||
|
||||
tasks.push(task);
|
||||
tasks.push(tool_result.output);
|
||||
}
|
||||
|
||||
Ok((tasks, tool_names))
|
||||
@@ -305,6 +305,6 @@ impl Tool for BatchTool {
|
||||
}
|
||||
|
||||
Ok(formatted_results.trim().to_string())
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{OutlineItem, ParseStatus, Point};
|
||||
@@ -129,10 +129,10 @@ impl Tool for CodeSymbolsTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let regex = match input.regex {
|
||||
@@ -141,7 +141,7 @@ impl Tool for CodeSymbolsTool {
|
||||
.build()
|
||||
{
|
||||
Ok(regex) => Some(regex),
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))),
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))).into(),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
@@ -149,7 +149,7 @@ impl Tool for CodeSymbolsTool {
|
||||
cx.spawn(async move |cx| match input.path {
|
||||
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
|
||||
None => project_symbols(project, regex, input.offset, cx).await,
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -77,10 +77,10 @@ impl Tool for CopyPathTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let copy_task = project.update(cx, |project, cx| {
|
||||
match project
|
||||
@@ -116,6 +116,6 @@ impl Tool for CopyPathTool {
|
||||
err
|
||||
)),
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -68,14 +68,14 @@ impl Tool for CreateDirectoryTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(),
|
||||
};
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
@@ -88,6 +88,6 @@ impl Tool for CreateDirectoryTool {
|
||||
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created directory {destination_path}"))
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -73,14 +73,14 @@ impl Tool for CreateFileTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(),
|
||||
};
|
||||
let contents: Arc<str> = input.contents.as_str().into();
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
@@ -105,6 +105,6 @@ impl Tool for CreateFileTool {
|
||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created file {destination_path}"))
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -63,15 +63,15 @@ impl Tool for DeletePathTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
Ok(input) => input.path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)));
|
||||
))).into();
|
||||
};
|
||||
|
||||
let Some(worktree) = project
|
||||
@@ -80,7 +80,7 @@ impl Tool for DeletePathTool {
|
||||
else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)));
|
||||
))).into();
|
||||
};
|
||||
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
@@ -131,6 +131,6 @@ impl Tool for DeletePathTool {
|
||||
"Couldn't delete {path_str} because that path isn't in this project."
|
||||
)),
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -83,14 +83,14 @@ impl Tool for DiagnosticsTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
{
|
||||
Some(path) if !path.is_empty() => {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project",))).into();
|
||||
};
|
||||
|
||||
let buffer =
|
||||
@@ -124,7 +124,7 @@ impl Tool for DiagnosticsTool {
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
_ => {
|
||||
let project = project.read(cx);
|
||||
@@ -155,9 +155,9 @@ impl Tool for DiagnosticsTool {
|
||||
});
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
Task::ready(Ok(output)).into()
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string())).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
@@ -146,10 +146,10 @@ impl Tool for FetchTool {
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<FetchToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let text = cx.background_spawn({
|
||||
@@ -165,6 +165,6 @@ impl Tool for FetchTool {
|
||||
}
|
||||
|
||||
Ok(text)
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -159,10 +159,10 @@ impl Tool for FindReplaceFileTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
@@ -253,6 +253,6 @@ impl Tool for FindReplaceFileTool {
|
||||
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
|
||||
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -77,10 +77,10 @@ impl Tool for ListDirectoryTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
// Sometimes models will return these even though we tell it to give a path and not a glob.
|
||||
@@ -101,26 +101,26 @@ impl Tool for ListDirectoryTool {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
return Task::ready(Ok(output));
|
||||
return Task::ready(Ok(output)).into();
|
||||
}
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into();
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Worktree not found")));
|
||||
return Task::ready(Err(anyhow!("Worktree not found"))).into();
|
||||
};
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
|
||||
};
|
||||
|
||||
if !entry.is_dir() {
|
||||
return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
|
||||
return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -133,8 +133,8 @@ impl Tool for ListDirectoryTool {
|
||||
.unwrap();
|
||||
}
|
||||
if output.is_empty() {
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path)));
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path))).into();
|
||||
}
|
||||
Task::ready(Ok(output))
|
||||
Task::ready(Ok(output)).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -90,10 +90,10 @@ impl Tool for MovePathTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<MovePathToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let rename_task = project.update(cx, |project, cx| {
|
||||
match project
|
||||
@@ -127,6 +127,6 @@ impl Tool for MovePathTool {
|
||||
err
|
||||
)),
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -60,10 +60,10 @@ impl Tool for NowTool {
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let now = match input.timezone {
|
||||
@@ -72,6 +72,6 @@ impl Tool for NowTool {
|
||||
};
|
||||
let text = format!("The current datetime is {now}.");
|
||||
|
||||
Task::ready(Ok(text))
|
||||
Task::ready(Ok(text)).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -53,16 +53,16 @@ impl Tool for OpenTool {
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input: OpenToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.background_spawn(async move {
|
||||
open::that(&input.path_or_url).context("Failed to open URL or file path")?;
|
||||
|
||||
Ok(format!("Successfully opened {}", input.path_or_url))
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -71,10 +71,10 @@ impl Tool for PathSearchTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.glob),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let path_matcher = match PathMatcher::new([
|
||||
@@ -82,7 +82,7 @@ impl Tool for PathSearchTool {
|
||||
if glob.is_empty() { "*" } else { &glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
|
||||
};
|
||||
let snapshots: Vec<Snapshot> = project
|
||||
.read(cx)
|
||||
@@ -136,6 +136,6 @@ impl Tool for PathSearchTool {
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -88,14 +88,14 @@ impl Tool for ReadFileTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,)));
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,))).into();
|
||||
};
|
||||
|
||||
let file_path = input.path.clone();
|
||||
@@ -146,6 +146,6 @@ impl Tool for ReadFileTool {
|
||||
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
|
||||
}
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
@@ -92,13 +92,13 @@ impl Tool for RegexSearchTool {
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let (offset, regex, case_sensitive) =
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.regex, input.case_sensitive),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
@@ -106,17 +106,18 @@ impl Tool for RegexSearchTool {
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
PathMatcher::default(),
|
||||
PathMatcher::default(),
|
||||
None,
|
||||
) {
|
||||
Ok(query) => query,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
Err(error) => return Task::ready(Err(error)).into(),
|
||||
};
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
let output = cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -200,6 +201,7 @@ impl Tool for RegexSearchTool {
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
})
|
||||
});
|
||||
output.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -122,10 +122,10 @@ impl Tool for SymbolInfoTool {
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
@@ -205,7 +205,7 @@ impl Tool for SymbolInfoTool {
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use anyhow::anyhow;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -51,11 +51,11 @@ impl Tool for ThinkingTool {
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
// This tool just "thinks out loud" and doesn't perform any actions.
|
||||
Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
|
||||
Ok(_input) => Ok("Finished thinking.".to_string()),
|
||||
Err(err) => Err(anyhow!(err)),
|
||||
})
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolSource};
|
||||
use anyhow::{anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -76,7 +76,7 @@ impl Tool for ContextServerTool {
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
) -> ToolResult {
|
||||
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
|
||||
let tool_name = self.tool.name.clone();
|
||||
let server_clone = server.clone();
|
||||
@@ -115,9 +115,9 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}).into()
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("Context server not found")))
|
||||
Task::ready(Err(anyhow!("Context server not found"))).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6365,20 +6365,9 @@ impl Editor {
|
||||
breakpoint_display_points
|
||||
}
|
||||
|
||||
fn edit_breakpoint_context_menu(
|
||||
fn breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn basic_breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
edit_menu_position: (Anchor, gpui::Point<Pixels>),
|
||||
is_edit_menu: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
@@ -6396,6 +6385,12 @@ impl Editor {
|
||||
.breakpoint_at_row(row, window, cx)
|
||||
.map(|(anchor, bp)| (anchor, Arc::from(bp)));
|
||||
|
||||
let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) {
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Set Log Breakpoint"
|
||||
};
|
||||
|
||||
let condition_breakpoint_msg = if breakpoint
|
||||
.as_ref()
|
||||
.is_some_and(|bp| bp.1.condition.is_some())
|
||||
@@ -6423,13 +6418,9 @@ impl Editor {
|
||||
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
|
||||
|
||||
let breakpoint_is_at_row = breakpoint.is_some();
|
||||
let log_editor = cx.new(|cx| {
|
||||
let mut log_editor = Editor::single_line(window, cx);
|
||||
if let Some(text) = &breakpoint.as_ref().and_then(|bp| bp.1.message.clone()) {
|
||||
log_editor.insert(text.as_ref(), window, cx);
|
||||
}
|
||||
log_editor
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
BreakpointState::Disabled => Some("Enable"),
|
||||
});
|
||||
|
||||
let (anchor, breakpoint) =
|
||||
@@ -6453,73 +6444,8 @@ impl Editor {
|
||||
})
|
||||
.separator()
|
||||
})
|
||||
.entry("edit breakpoint", None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let context_menu =
|
||||
editor.edit_breakpoint_context_menu(anchor, window, cx);
|
||||
let (source, clicked_point) = edit_menu_position;
|
||||
|
||||
editor.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
editor,
|
||||
source,
|
||||
clicked_point,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.when(breakpoint_is_at_row, |this| {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
this.custom_row(move |window, cx| {
|
||||
let breakpoint = weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.breakpoint_at_row(row, window, cx).map(|bp| bp.1)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| breakpoint.as_ref().clone());
|
||||
|
||||
let is_enabled = match breakpoint.is_enabled() {
|
||||
true => ToggleState::Selected,
|
||||
false => ToggleState::Unselected,
|
||||
};
|
||||
|
||||
ui::CheckboxWithLabel::new(
|
||||
"enable-breakpoint",
|
||||
Label::new("Enable"),
|
||||
is_enabled,
|
||||
{
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |_, _, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.clone(),
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(IconPosition::End)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.when(!breakpoint_is_at_row, |this| {
|
||||
this.entry(set_breakpoint_msg, None, {
|
||||
.when_some(toggle_state_msg, |this, msg| {
|
||||
this.entry(msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
@@ -6528,7 +6454,7 @@ impl Editor {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
@@ -6536,72 +6462,69 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(is_edit_menu, |this| {
|
||||
this.custom_entry(
|
||||
{
|
||||
breakpoint_message_editor_element(
|
||||
anchor,
|
||||
breakpoint.clone(),
|
||||
BreakpointMessageKind::Log,
|
||||
log_editor.clone(),
|
||||
weak_editor.clone(),
|
||||
)
|
||||
},
|
||||
{
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
let log_editor = log_editor.clone();
|
||||
|
||||
move |_, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(
|
||||
log_message.into(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.entry(set_breakpoint_msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(log_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Log,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(condition_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Condition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::HitCondition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
|
||||
// .entry(condition_breakpoint_msg, None, {
|
||||
// let breakpoint = breakpoint.clone();
|
||||
// let weak_editor = weak_editor.clone();
|
||||
// move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::Condition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// }
|
||||
// })
|
||||
// .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::HitCondition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8822,48 +8745,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint_menu_editor(
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: AbsoluteLength::Rems(Rems(0.75)),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
background_color: Some(theme.colors().editor_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let element = EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
div()
|
||||
.rounded_sm()
|
||||
.when(
|
||||
editor.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| this.border_color(theme.colors().border_focused),
|
||||
)
|
||||
.child(element)
|
||||
.min_w(Pixels(300.0))
|
||||
.size_full()
|
||||
.bg(theme.colors().editor_background)
|
||||
.h_4()
|
||||
}
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
display_row: DisplayRow,
|
||||
@@ -8881,13 +8762,7 @@ impl Editor {
|
||||
.snapshot(cx)
|
||||
.anchor_before(Point::new(display_row.0, 0u32));
|
||||
|
||||
let context_menu = self.basic_breakpoint_context_menu(
|
||||
position.unwrap_or(source),
|
||||
(source, clicked_point),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
@@ -18134,58 +18009,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
enum BreakpointMessageKind {
|
||||
Log,
|
||||
Conditional,
|
||||
HitConditional,
|
||||
}
|
||||
|
||||
fn breakpoint_message_editor_element(
|
||||
anchor: Anchor,
|
||||
breakpoint: Arc<Breakpoint>,
|
||||
message_kind: BreakpointMessageKind,
|
||||
message_editor: Entity<Editor>,
|
||||
weak_editor: WeakEntity<Editor>,
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyElement {
|
||||
let log_editor = message_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
move |window, cx| {
|
||||
let label = Label::new("Log message");
|
||||
let log_editor = log_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.size_full()
|
||||
.child(label)
|
||||
.child(Editor::render_breakpoint_menu_editor(
|
||||
&log_editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_action(move |_: &menu::Confirm, _, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
this.mouse_context_menu = None;
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
// Consider user intent and default settings
|
||||
fn choose_completion_range(
|
||||
completion: &Completion,
|
||||
|
||||
@@ -3890,15 +3890,7 @@ impl EditorElement {
|
||||
)
|
||||
})?;
|
||||
|
||||
element.prepaint_as_root(
|
||||
position,
|
||||
Size {
|
||||
width: AvailableSpace::Definite(Pixels(100.)),
|
||||
height: AvailableSpace::MinContent,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
|
||||
Some(element)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1540,8 +1540,24 @@ impl SearchableItem for Editor {
|
||||
let text = self.buffer.read(cx);
|
||||
let text = text.snapshot(cx);
|
||||
let mut edits = vec![];
|
||||
let mut last_point: Option<Point> = None;
|
||||
|
||||
for m in matches {
|
||||
let point = m.start.to_point(&text);
|
||||
let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
|
||||
|
||||
// Check if the row for the current match is different from the last
|
||||
// match. If that's not the case and we're still replacing matches
|
||||
// in the same row/line, skip this match if the `one_match_per_line`
|
||||
// option is enabled.
|
||||
if last_point.is_none() {
|
||||
last_point = Some(point);
|
||||
} else if last_point.is_some() && point.row != last_point.unwrap().row {
|
||||
last_point = Some(point);
|
||||
} else if query.one_match_per_line().is_some_and(|enabled| enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let text: Cow<_> = if text.len() == 1 {
|
||||
text.first().cloned().unwrap().into()
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{MacDisplay, NSRange, NSStringExt, ns_string, renderer};
|
||||
use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
|
||||
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
@@ -1021,11 +1021,8 @@ impl PlatformWindow for MacWindow {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let opaque = if background_appearance == WindowBackgroundAppearance::Opaque {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
|
||||
|
||||
unsafe {
|
||||
this.native_window.setOpaque_(opaque);
|
||||
// Shadows for transparent windows cause artifacts and performance issues
|
||||
@@ -1981,14 +1978,11 @@ extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
|
||||
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
if send_new_event(
|
||||
send_new_event(
|
||||
&window_state,
|
||||
PlatformInput::FileDrop(FileDropEvent::Submit { position }),
|
||||
) {
|
||||
YES
|
||||
} else {
|
||||
NO
|
||||
}
|
||||
)
|
||||
.to_objc()
|
||||
}
|
||||
|
||||
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {
|
||||
|
||||
@@ -424,6 +424,8 @@ impl LocalLspStore {
|
||||
let mut binary = binary_result?;
|
||||
let mut shell_env = delegate.shell_env().await;
|
||||
|
||||
shell_env.extend(binary.env.unwrap_or_default());
|
||||
|
||||
if let Some(settings) = settings {
|
||||
if let Some(arguments) = settings.arguments {
|
||||
binary.arguments = arguments.into_iter().map(Into::into).collect();
|
||||
|
||||
@@ -334,6 +334,10 @@ impl ProjectPath {
|
||||
path: Path::new("").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: &ProjectPath) -> bool {
|
||||
self.worktree_id == other.worktree_id && self.path.starts_with(&other.path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -71,6 +71,7 @@ pub enum SearchQuery {
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
one_match_per_line: bool,
|
||||
inner: SearchInputs,
|
||||
},
|
||||
}
|
||||
@@ -116,6 +117,7 @@ impl SearchQuery {
|
||||
whole_word: bool,
|
||||
case_sensitive: bool,
|
||||
include_ignored: bool,
|
||||
one_match_per_line: bool,
|
||||
files_to_include: PathMatcher,
|
||||
files_to_exclude: PathMatcher,
|
||||
buffers: Option<Vec<Entity<Buffer>>>,
|
||||
@@ -156,6 +158,7 @@ impl SearchQuery {
|
||||
case_sensitive,
|
||||
include_ignored,
|
||||
inner,
|
||||
one_match_per_line,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +169,7 @@ impl SearchQuery {
|
||||
message.whole_word,
|
||||
message.case_sensitive,
|
||||
message.include_ignored,
|
||||
false,
|
||||
deserialize_path_matches(&message.files_to_include)?,
|
||||
deserialize_path_matches(&message.files_to_exclude)?,
|
||||
None, // search opened only don't need search remote
|
||||
@@ -459,6 +463,19 @@ impl SearchQuery {
|
||||
Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this search should replace only one match per line, instead of
|
||||
/// all matches.
|
||||
/// Returns `None` for text searches, as only regex searches support this
|
||||
/// option.
|
||||
pub fn one_match_per_line(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::Regex {
|
||||
one_match_per_line, ..
|
||||
} => Some(*one_match_per_line),
|
||||
Self::Text { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<PathMatcher> {
|
||||
|
||||
@@ -1231,6 +1231,8 @@ impl BufferSearchBar {
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
false,
|
||||
self.search_options
|
||||
.contains(SearchOptions::ONE_MATCH_PER_LINE),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
None,
|
||||
|
||||
@@ -1053,6 +1053,8 @@ impl ProjectSearchView {
|
||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||
self.search_options
|
||||
.contains(SearchOptions::ONE_MATCH_PER_LINE),
|
||||
included_files,
|
||||
excluded_files,
|
||||
open_buffers,
|
||||
|
||||
@@ -48,6 +48,7 @@ bitflags! {
|
||||
const CASE_SENSITIVE = 0b010;
|
||||
const INCLUDE_IGNORED = 0b100;
|
||||
const REGEX = 0b1000;
|
||||
const ONE_MATCH_PER_LINE = 0b100000;
|
||||
/// If set, reverse direction when finding the active match
|
||||
const BACKWARDS = 0b10000;
|
||||
}
|
||||
|
||||
@@ -445,6 +445,8 @@ impl Vim {
|
||||
}
|
||||
let vim = cx.entity().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let mut options = SearchOptions::REGEX;
|
||||
|
||||
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
|
||||
return;
|
||||
};
|
||||
@@ -453,7 +455,6 @@ impl Vim {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut options = SearchOptions::REGEX;
|
||||
if replacement.is_case_sensitive {
|
||||
options.set(SearchOptions::CASE_SENSITIVE, true)
|
||||
}
|
||||
@@ -468,6 +469,11 @@ impl Vim {
|
||||
search_bar.is_contains_uppercase(&search),
|
||||
);
|
||||
}
|
||||
|
||||
if !replacement.should_replace_all {
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
|
||||
}
|
||||
|
||||
search_bar.set_replacement(Some(&replacement.replacement), cx);
|
||||
Some(search_bar.search(&search, Some(options), window, cx))
|
||||
});
|
||||
@@ -476,29 +482,35 @@ impl Vim {
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
search.await?;
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
if replacement.should_replace_all {
|
||||
search_bar.select_last_match(window, cx);
|
||||
search_bar.replace_all(&Default::default(), window, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
editor
|
||||
.update(cx, |editor, cx| editor.clear_search_within_ranges(cx))
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
vim.update(cx, |vim, cx| {
|
||||
vim.move_cursor(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
search_bar.select_last_match(window, cx);
|
||||
search_bar.replace_all(&Default::default(), window, cx);
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
editor
|
||||
.update(cx, |editor, cx| editor.clear_search_within_ranges(cx))
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
vim.update(cx, |vim, cx| {
|
||||
vim.move_cursor(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Disable the `ONE_MATCH_PER_LINE` search option when finished, as
|
||||
// this is not properly supported outside of vim mode, and
|
||||
// not disabling it makes the "Replace All Matches" button
|
||||
// actually replace only the first match on each line.
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
|
||||
search_bar.set_search_options(options, cx);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -564,15 +576,16 @@ impl Replacement {
|
||||
let mut replacement = Replacement {
|
||||
search,
|
||||
replacement,
|
||||
should_replace_all: true,
|
||||
should_replace_all: false,
|
||||
is_case_sensitive: true,
|
||||
};
|
||||
|
||||
for c in flags.chars() {
|
||||
match c {
|
||||
'g' | 'I' => {}
|
||||
'g' => replacement.should_replace_all = true,
|
||||
'c' | 'n' => replacement.should_replace_all = false,
|
||||
'i' => replacement.is_case_sensitive = false,
|
||||
'I' => replacement.is_case_sensitive = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1155,8 +1155,7 @@ mod test {
|
||||
fox ˇjumps over
|
||||
the lazy dog"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-v shift-4 shift-y")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick brown
|
||||
ˇfox jumps over
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}}
|
||||
{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"shift-4"}
|
||||
{"Key":"$"}
|
||||
{"Key":"shift-y"}
|
||||
{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}
|
||||
|
||||
@@ -1172,6 +1172,31 @@ impl Worktree {
|
||||
pub fn is_single_file(&self) -> bool {
|
||||
self.root_dir().is_none()
|
||||
}
|
||||
|
||||
/// For visible worktrees, returns the path with the worktree name as the first component.
|
||||
/// Otherwise, returns an absolute path.
|
||||
pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
|
||||
let mut full_path = PathBuf::new();
|
||||
|
||||
if self.is_visible() {
|
||||
full_path.push(self.root_name());
|
||||
} else {
|
||||
let path = self.abs_path();
|
||||
|
||||
if self.is_local() && path.starts_with(home_dir().as_path()) {
|
||||
full_path.push("~");
|
||||
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
|
||||
} else {
|
||||
full_path.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
if worktree_relative_path.components().next().is_some() {
|
||||
full_path.push(&worktree_relative_path);
|
||||
}
|
||||
|
||||
full_path
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalWorktree {
|
||||
@@ -3229,27 +3254,7 @@ impl language::File for File {
|
||||
}
|
||||
|
||||
fn full_path(&self, cx: &App) -> PathBuf {
|
||||
let mut full_path = PathBuf::new();
|
||||
let worktree = self.worktree.read(cx);
|
||||
|
||||
if worktree.is_visible() {
|
||||
full_path.push(worktree.root_name());
|
||||
} else {
|
||||
let path = worktree.abs_path();
|
||||
|
||||
if worktree.is_local() && path.starts_with(home_dir().as_path()) {
|
||||
full_path.push("~");
|
||||
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
|
||||
} else {
|
||||
full_path.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
if self.path.components().next().is_some() {
|
||||
full_path.push(&self.path);
|
||||
}
|
||||
|
||||
full_path
|
||||
self.worktree.read(cx).full_path(&self.path)
|
||||
}
|
||||
|
||||
/// Returns the last component of this handle's absolute path. If this handle refers to the root
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.182.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -166,6 +166,7 @@ Zed's vim mode includes some features that are usually provided by very popular
|
||||
- You can add key bindings to your keymap to navigate "camelCase" names. [Head down to the Optional key bindings](#optional-key-bindings) section to learn how.
|
||||
- You can use `gr` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister).
|
||||
- You can use `cx` for [vim-exchange](https://github.com/tommcdo/vim-exchange) functionality. Note that it does not have a default binding in visual mode, but you can add one to your keymap (refer to the [optional key bindings](#optional-key-bindings) section).
|
||||
- You can navigate to indent depths relative to your cursor with the [indent wise](https://github.com/jeetsukumaran/vim-indentwise) plugin `[-`, `]-`, `[+`, `]+`, `[=`, `]=`.
|
||||
|
||||
## Command palette
|
||||
|
||||
@@ -199,6 +200,7 @@ This table shows commands for managing windows, tabs, and panes. As commands don
|
||||
| `:tabn[ext]` | Go to the next tab |
|
||||
| `:tabp[rev]` | Go to previous tab |
|
||||
| `:tabc[lose]` | Close the current tab |
|
||||
| `:ls` | Show all buffers |
|
||||
|
||||
> **Note:** The `!` character is used to force the command to execute without saving changes or prompting before overwriting a file.
|
||||
|
||||
@@ -267,6 +269,16 @@ These commands help you edit text.
|
||||
| `:s[ort] [i]` | Sort the current selection (with i, case-insensitively) |
|
||||
| `:y[ank]` | Yank (copy) the current selection or line |
|
||||
|
||||
### Set
|
||||
|
||||
These commands modify editor options locally for the current buffer.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `:se[t] [no]wrap` | Lines longer than the width of the window will wrap and displaying continues on the next line |
|
||||
| `:se[t] [no]nu[mber]` | Print the line number in front of each line |
|
||||
| `:se[t] [no]r[elative]nu[mber]` | Changes the displayed number to be relative to the cursor |
|
||||
|
||||
### Command mnemonics
|
||||
|
||||
As any Zed command is available, you may find that it's helpful to remember mnemonics that run the correct command. For example:
|
||||
@@ -498,6 +510,7 @@ Here's an example of these settings changed:
|
||||
```json
|
||||
{
|
||||
"vim": {
|
||||
"default_mode": "insert",
|
||||
"use_system_clipboard": "never",
|
||||
"use_multiline_find": true,
|
||||
"use_smartcase_find": true,
|
||||
|
||||
Reference in New Issue
Block a user