Compare commits
1 Commits
fix-code-a
...
update-use
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd676ffc4d |
48
Cargo.lock
generated
48
Cargo.lock
generated
@@ -1166,7 +1166,7 @@ version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50"
|
||||
dependencies = [
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite 2.6.0",
|
||||
]
|
||||
@@ -1180,7 +1180,7 @@ dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite 2.6.0",
|
||||
"once_cell",
|
||||
@@ -1192,7 +1192,7 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca"
|
||||
dependencies = [
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
@@ -1204,15 +1204,6 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
|
||||
dependencies = [
|
||||
"event-listener 2.5.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.1"
|
||||
@@ -1252,7 +1243,7 @@ checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel 2.3.1",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
@@ -1281,7 +1272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
@@ -1302,7 +1293,7 @@ dependencies = [
|
||||
"async-channel 1.9.0",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
@@ -6537,15 +6528,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
@@ -11052,7 +11034,7 @@ dependencies = [
|
||||
"ashpd 0.12.0",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"cbc",
|
||||
"cipher",
|
||||
@@ -15753,7 +15735,7 @@ dependencies = [
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"async-net",
|
||||
"async-process",
|
||||
"blocking",
|
||||
@@ -20867,7 +20849,6 @@ name = "worktree"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-lock 2.8.0",
|
||||
"clock",
|
||||
"collections",
|
||||
"fs",
|
||||
@@ -20898,17 +20879,6 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
@@ -21180,7 +21150,7 @@ dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock 3.4.1",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
|
||||
@@ -222,7 +222,7 @@ members = [
|
||||
|
||||
"tooling/perf",
|
||||
"tooling/workspace-hack",
|
||||
"tooling/xtask", "crates/fs_benchmarks", "crates/worktree_benchmarks",
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
|
||||
@@ -455,7 +455,6 @@ async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
async-fs = "2.1"
|
||||
async-lock = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
|
||||
@@ -491,8 +491,8 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above
|
||||
"shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below
|
||||
"shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
|
||||
@@ -539,10 +539,10 @@
|
||||
"bindings": {
|
||||
"cmd-[": "editor::Outdent",
|
||||
"cmd-]": "editor::Indent",
|
||||
"cmd-ctrl-p": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], // Insert cursor above
|
||||
"cmd-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }],
|
||||
"cmd-ctrl-n": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], // Insert cursor below
|
||||
"cmd-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"cmd-ctrl-p": "editor::AddSelectionAbove", // Insert cursor above
|
||||
"cmd-alt-up": "editor::AddSelectionAbove",
|
||||
"cmd-ctrl-n": "editor::AddSelectionBelow", // Insert cursor below
|
||||
"cmd-alt-down": "editor::AddSelectionBelow",
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
|
||||
@@ -500,8 +500,8 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"ctrl-shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above
|
||||
"ctrl-shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below
|
||||
"ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // editor:add-selection-below
|
||||
"alt-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // editor:add-selection-above
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
|
||||
"ctrl-j": "editor::JoinLines", // editor:join-lines
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }],
|
||||
"ctrl-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }],
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"alt-enter": "editor::Newline",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }],
|
||||
"ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }],
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"cmd-ctrl-up": "editor::MoveLineUp",
|
||||
"cmd-ctrl-down": "editor::MoveLineDown",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
|
||||
@@ -95,6 +95,8 @@
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g B": "editor::BlameHover",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToDeclaration",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
@@ -498,8 +500,8 @@
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
"c": "vim::Substitute",
|
||||
"shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }]
|
||||
"shift-c": "editor::AddSelectionBelow",
|
||||
"alt-shift-c": "editor::AddSelectionAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -809,7 +811,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VimControl && !menu || !Editor && !Terminal",
|
||||
"context": "VimControl || !Editor && !Terminal",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -863,9 +865,7 @@
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab"
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -724,9 +724,7 @@
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false,
|
||||
// Whether to hide the hidden entries in the project panel.
|
||||
"hide_hidden": false,
|
||||
// Whether to automatically open files when pasting them in the project panel.
|
||||
"open_file_on_paste": true
|
||||
"hide_hidden": false
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
|
||||
@@ -174,16 +174,11 @@ impl Render for ModeSelector {
|
||||
|
||||
let this = cx.entity();
|
||||
|
||||
let icon = if self.menu_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(icon)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -5,12 +5,12 @@ use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
||||
use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
|
||||
ListItemSpacing, prelude::*,
|
||||
AnyElement, App, Context, DocumentationAside, DocumentationEdge, DocumentationSide,
|
||||
IntoElement, ListItem, ListItemSpacing, SharedString, Window, prelude::*, rems,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -278,6 +278,36 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
@@ -287,7 +317,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
let description = description.clone();
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Top,
|
||||
DocumentationEdge::Bottom,
|
||||
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -57,26 +57,30 @@ impl Render for AcpModelSelectorPopover {
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let (color, icon) = if self.menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
let color = if self.menu_handle.is_deployed() {
|
||||
Color::Accent
|
||||
} else {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
|
||||
@@ -292,8 +292,6 @@ pub struct AcpThreadView {
|
||||
resume_thread_metadata: Option<DbThreadMetadata>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 5],
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning: bool,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -399,10 +397,6 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
||||
== Some(crate::ExternalAgent::Codex);
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
workspace: workspace.clone(),
|
||||
@@ -445,8 +439,6 @@ impl AcpThreadView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5036,49 +5028,6 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.show_codex_windows_warning {
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.title("Codex on Windows")
|
||||
.description(
|
||||
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
|
||||
)
|
||||
.actions_slot(
|
||||
Button::new("open-wsl-modal", "Open in WSL")
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.dismiss_action(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.show_codex_windows_warning = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
@@ -5566,16 +5515,6 @@ impl Render for AcpThreadView {
|
||||
_ => this,
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children({
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.render_codex_windows_warning(cx)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::<Empty>::new()
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{Entity, FocusHandle, SharedString};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
@@ -70,11 +70,6 @@ impl Render for AgentModelSelector {
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let provider_icon = model.as_ref().map(|model| model.provider.icon());
|
||||
let color = if self.menu_handle.is_deployed() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -82,18 +77,17 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |window, cx| {
|
||||
@@ -105,14 +99,10 @@ impl Render for AgentModelSelector {
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::TopRight,
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
})
|
||||
.render(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +222,12 @@ enum WhichFontSize {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
NativeAgent,
|
||||
Zed,
|
||||
TextThread,
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
@@ -236,7 +237,8 @@ pub enum AgentType {
|
||||
impl AgentType {
|
||||
fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
|
||||
Self::Zed | Self::TextThread => "Zed Agent".into(),
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Codex => "Codex".into(),
|
||||
@@ -246,7 +248,7 @@ impl AgentType {
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Codex => Some(IconName::AiOpenAi),
|
||||
@@ -811,7 +813,7 @@ impl AgentPanel {
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct LastUsedExternalAgent {
|
||||
agent: crate::ExternalAgent,
|
||||
}
|
||||
@@ -852,18 +854,18 @@ impl AgentPanel {
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.map(|agent| agent.agent)
|
||||
.unwrap_or(ExternalAgent::NativeAgent)
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
|
||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||
}
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
@@ -1343,6 +1345,15 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
NewThread {
|
||||
from_thread_id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AgentType::TextThread => {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}
|
||||
|
||||
@@ -161,9 +161,10 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||
}
|
||||
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExternalAgent {
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
@@ -183,13 +184,13 @@ fn placeholder_command() -> AgentServerCommand {
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
|
||||
match server.telemetry_id() {
|
||||
"gemini-cli" => Some(Self::Gemini),
|
||||
"claude-code" => Some(Self::ClaudeCode),
|
||||
"codex" => Some(Self::Codex),
|
||||
"zed" => Some(Self::NativeAgent),
|
||||
_ => None,
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Codex => "codex",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,16 +144,10 @@ impl Render for ProfileSelector {
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let icon = if self.picker_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
let trigger_button = Button::new("profile-selector", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(icon)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -1977,9 +1977,7 @@ impl TextThreadEditor {
|
||||
cx.entity().downgrade(),
|
||||
IconButton::new("trigger", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.selected_icon_color(Color::Accent)
|
||||
.selected_style(ButtonStyle::Filled),
|
||||
.icon_color(Color::Muted),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Add Context",
|
||||
@@ -2054,27 +2052,30 @@ impl TextThreadEditor {
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
} else {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(Icon::new(icon).color(color).size(IconSize::XSmall)),
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
@@ -2085,7 +2086,7 @@ impl TextThreadEditor {
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
gpui::Corner::BottomLeft,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
|
||||
@@ -322,9 +322,6 @@ pub struct LanguageModel {
|
||||
pub supports_images: bool,
|
||||
pub supports_thinking: bool,
|
||||
pub supports_max_mode: bool,
|
||||
// only used by OpenAI and xAI
|
||||
#[serde(default)]
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -262,15 +262,11 @@ impl TransportDelegate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up logs by trimming unnecessary whitespace/newlines before inserting into log.
|
||||
let line = line.trim();
|
||||
|
||||
log::debug!("stderr: {line}");
|
||||
|
||||
for (kind, handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Adapter) {
|
||||
handler(iokind, None, line);
|
||||
handler(iokind, None, line.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -377,12 +377,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
command = Some(path);
|
||||
};
|
||||
let mut json_config = config.config.clone();
|
||||
|
||||
// Enable info level for CodeLLDB by default.
|
||||
// Logs can then be viewed in our DAP logs.
|
||||
let mut envs = collections::HashMap::default();
|
||||
envs.insert("RUST_LOG".to_string(), "info".to_string());
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
@@ -407,7 +401,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
request_args: self
|
||||
.request_args(delegate, json_config, &config.label)
|
||||
.await?,
|
||||
envs,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -318,24 +318,6 @@ pub struct GoToPreviousDiagnostic {
|
||||
pub severity: GoToDiagnosticSeverityFilter,
|
||||
}
|
||||
|
||||
/// Adds a cursor above the current selection.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AddSelectionAbove {
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_soft_wrap: bool,
|
||||
}
|
||||
|
||||
/// Adds a cursor below the current selection.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AddSelectionBelow {
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_soft_wrap: bool,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[
|
||||
@@ -363,6 +345,10 @@ actions!(
|
||||
/// Accepts a partial edit prediction.
|
||||
#[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])]
|
||||
AcceptPartialEditPrediction,
|
||||
/// Adds a cursor above the current selection.
|
||||
AddSelectionAbove,
|
||||
/// Adds a cursor below the current selection.
|
||||
AddSelectionBelow,
|
||||
/// Applies all diff hunks in the editor.
|
||||
ApplyAllDiffHunks,
|
||||
/// Applies the diff hunk at the current position.
|
||||
|
||||
@@ -1401,26 +1401,6 @@ impl DisplaySnapshot {
|
||||
pub fn excerpt_header_height(&self) -> u32 {
|
||||
self.block_snapshot.excerpt_header_height
|
||||
}
|
||||
|
||||
/// Given a `DisplayPoint`, returns another `DisplayPoint` corresponding to
|
||||
/// the start of the buffer row that is a given number of buffer rows away
|
||||
/// from the provided point.
|
||||
///
|
||||
/// This moves by buffer rows instead of display rows, a distinction that is
|
||||
/// important when soft wrapping is enabled.
|
||||
pub fn start_of_relative_buffer_row(&self, point: DisplayPoint, times: isize) -> DisplayPoint {
|
||||
let start = self.display_point_to_fold_point(point, Bias::Left);
|
||||
let target = start.row() as isize + times;
|
||||
let new_row = (target.max(0) as u32).min(self.fold_snapshot().max_point().row());
|
||||
|
||||
self.clip_point(
|
||||
self.fold_point_to_display_point(
|
||||
self.fold_snapshot()
|
||||
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
|
||||
@@ -14236,29 +14236,23 @@ impl Editor {
|
||||
|
||||
pub fn add_selection_above(
|
||||
&mut self,
|
||||
action: &AddSelectionAbove,
|
||||
_: &AddSelectionAbove,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_selection(true, action.skip_soft_wrap, window, cx);
|
||||
self.add_selection(true, window, cx);
|
||||
}
|
||||
|
||||
pub fn add_selection_below(
|
||||
&mut self,
|
||||
action: &AddSelectionBelow,
|
||||
_: &AddSelectionBelow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_selection(false, action.skip_soft_wrap, window, cx);
|
||||
self.add_selection(false, window, cx);
|
||||
}
|
||||
|
||||
fn add_selection(
|
||||
&mut self,
|
||||
above: bool,
|
||||
skip_soft_wrap: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -14345,19 +14339,12 @@ impl Editor {
|
||||
};
|
||||
|
||||
let mut maybe_new_selection = None;
|
||||
let direction = if above { -1 } else { 1 };
|
||||
|
||||
while row != end_row {
|
||||
if skip_soft_wrap {
|
||||
row = display_map
|
||||
.start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
|
||||
.row();
|
||||
} else if above {
|
||||
if above {
|
||||
row.0 -= 1;
|
||||
} else {
|
||||
row.0 += 1;
|
||||
}
|
||||
|
||||
if let Some(new_selection) = self.selections.build_columnar_selection(
|
||||
&display_map,
|
||||
row,
|
||||
|
||||
@@ -12509,7 +12509,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
// Set up a buffer white some trailing whitespace and no trailing newline.
|
||||
cx.set_state(
|
||||
&[
|
||||
@@ -25682,83 +25681,6 @@ async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppC
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state(indoc!(
|
||||
r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
|
||||
Second line here"#
|
||||
));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
// Enable soft wrapping with a narrow width to force soft wrapping and
|
||||
// confirm that more than 2 rows are being displayed.
|
||||
editor.set_wrap_width(Some(100.0.into()), cx);
|
||||
assert!(editor.display_text(cx).lines().count() > 2);
|
||||
|
||||
editor.add_selection_below(
|
||||
&AddSelectionBelow {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
|
||||
]
|
||||
);
|
||||
|
||||
editor.add_selection_above(
|
||||
&AddSelectionAbove {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
editor.add_selection_below(
|
||||
&AddSelectionBelow {
|
||||
skip_soft_wrap: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
|
||||
]
|
||||
);
|
||||
|
||||
editor.add_selection_above(
|
||||
&AddSelectionAbove {
|
||||
skip_soft_wrap: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_document_colors(cx: &mut TestAppContext) {
|
||||
let expected_color = Rgba {
|
||||
|
||||
@@ -392,11 +392,6 @@ impl SelectionsCollection {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to build a selection in the provided `DisplayRow` within the
|
||||
/// same range as the provided range of `Pixels`.
|
||||
/// Returns `None` if the range is not empty but it starts past the line's
|
||||
/// length, meaning that the line isn't long enough to be contained within
|
||||
/// part of the provided range.
|
||||
pub fn build_columnar_selection(
|
||||
&mut self,
|
||||
display_map: &DisplaySnapshot,
|
||||
|
||||
@@ -7,7 +7,6 @@ pub mod fs_watcher;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
use futures::stream::iter;
|
||||
use gpui::App;
|
||||
use gpui::BackgroundExecutor;
|
||||
use gpui::Global;
|
||||
@@ -563,17 +562,12 @@ impl Fs for RealFs {
|
||||
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
let path = path.to_path_buf();
|
||||
self.executor
|
||||
.spawn(async move { Ok(std::fs::read_to_string(path)?) })
|
||||
.await
|
||||
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
|
||||
let path = path.to_path_buf();
|
||||
let bytes = self
|
||||
.executor
|
||||
.spawn(async move { std::fs::read(path) })
|
||||
.await?;
|
||||
let bytes = smol::unblock(|| std::fs::read(path)).await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
@@ -641,46 +635,30 @@ impl Fs for RealFs {
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).await?;
|
||||
}
|
||||
let path = path.to_owned();
|
||||
let contents = content.to_owned();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
std::fs::write(path, contents)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
smol::fs::write(path, content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
std::fs::canonicalize(&path).with_context(|| format!("canonicalizing {path:?}"))
|
||||
})
|
||||
Ok(smol::fs::canonicalize(path)
|
||||
.await
|
||||
.with_context(|| format!("canonicalizing {path:?}"))?)
|
||||
}
|
||||
|
||||
async fn is_file(&self, path: &Path) -> bool {
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) })
|
||||
smol::fs::metadata(path)
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_file())
|
||||
}
|
||||
|
||||
async fn is_dir(&self, path: &Path) -> bool {
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) })
|
||||
smol::fs::metadata(path)
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_dir())
|
||||
}
|
||||
|
||||
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
|
||||
let path_buf = path.to_owned();
|
||||
let symlink_metadata = match self
|
||||
.executor
|
||||
.spawn(async move { std::fs::symlink_metadata(&path_buf) })
|
||||
.await
|
||||
{
|
||||
let symlink_metadata = match smol::fs::symlink_metadata(path).await {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
return match (err.kind(), err.raw_os_error()) {
|
||||
@@ -691,28 +669,19 @@ impl Fs for RealFs {
|
||||
}
|
||||
};
|
||||
|
||||
let path_buf = path.to_path_buf();
|
||||
let path_exists = smol::unblock(move || {
|
||||
path_buf
|
||||
.try_exists()
|
||||
.with_context(|| format!("checking existence for path {path_buf:?}"))
|
||||
})
|
||||
.await?;
|
||||
let is_symlink = symlink_metadata.file_type().is_symlink();
|
||||
let metadata = if is_symlink {
|
||||
let path_buf = path.to_path_buf();
|
||||
let path_exists = self
|
||||
.executor
|
||||
.spawn(async move {
|
||||
path_buf
|
||||
.try_exists()
|
||||
.with_context(|| format!("checking existence for path {path_buf:?}"))
|
||||
})
|
||||
.await?;
|
||||
if path_exists {
|
||||
let path_buf = path.to_path_buf();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path_buf) })
|
||||
.await
|
||||
.with_context(|| "accessing symlink for path {path}")?
|
||||
} else {
|
||||
symlink_metadata
|
||||
}
|
||||
} else {
|
||||
symlink_metadata
|
||||
let metadata = match (is_symlink, path_exists) {
|
||||
(true, true) => smol::fs::metadata(path)
|
||||
.await
|
||||
.with_context(|| "accessing symlink for path {path}")?,
|
||||
_ => symlink_metadata,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -738,11 +707,7 @@ impl Fs for RealFs {
|
||||
}
|
||||
|
||||
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
|
||||
let path = path.to_owned();
|
||||
let path = self
|
||||
.executor
|
||||
.spawn(async move { std::fs::read_link(&path) })
|
||||
.await?;
|
||||
let path = smol::fs::read_link(path).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@@ -750,13 +715,7 @@ impl Fs for RealFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
|
||||
let path = path.to_owned();
|
||||
let result = iter(
|
||||
self.executor
|
||||
.spawn(async move { std::fs::read_dir(path) })
|
||||
.await?,
|
||||
)
|
||||
.map(|entry| match entry {
|
||||
let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
|
||||
Ok(entry) => Ok(entry.path()),
|
||||
Err(error) => Err(anyhow!("failed to read dir entry {error:?}")),
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "fs_benchmarks"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fs.workspace = true
|
||||
gpui = {workspace = true, features = ["windows-manifest"]}
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,32 +0,0 @@
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Application};
|
||||
fn main() {
|
||||
let Some(path_to_read) = std::env::args().nth(1) else {
|
||||
println!("Expected path to read as 1st argument.");
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = Application::headless().run(|cx| {
|
||||
let fs = fs::RealFs::new(None, cx.background_executor().clone());
|
||||
cx.background_spawn(async move {
|
||||
let timer = std::time::Instant::now();
|
||||
let result = fs.load_bytes(path_to_read.as_ref()).await;
|
||||
let elapsed = timer.elapsed();
|
||||
if let Err(e) = result {
|
||||
println!("Failed `load_bytes` after {elapsed:?} with error `{e}`");
|
||||
} else {
|
||||
println!("Took {elapsed:?} to read {} bytes", result.unwrap().len());
|
||||
};
|
||||
let timer = std::time::Instant::now();
|
||||
let result = fs.metadata(path_to_read.as_ref()).await;
|
||||
let elapsed = timer.elapsed();
|
||||
if let Err(e) = result {
|
||||
println!("Failed `metadata` after {elapsed:?} with error `{e}`");
|
||||
} else {
|
||||
println!("Took {elapsed:?} to query metadata");
|
||||
};
|
||||
std::process::exit(0);
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
@@ -374,6 +374,7 @@ impl DataTable {
|
||||
impl Render for DataTable {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.text_sm()
|
||||
.size_full()
|
||||
|
||||
@@ -20,6 +20,7 @@ impl Render for GradientViewer {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
|
||||
@@ -47,6 +47,7 @@ impl Render for ImageGallery {
|
||||
div()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.id("main")
|
||||
.font_family(".SystemUIFont")
|
||||
.text_color(gpui::black())
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.overflow_y_scroll()
|
||||
@@ -101,6 +102,7 @@ impl Render for ImageGallery {
|
||||
.child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
|
||||
div()
|
||||
.id("main")
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.text_color(gpui::black())
|
||||
.overflow_y_scroll()
|
||||
|
||||
@@ -328,6 +328,7 @@ impl Render for PaintingViewer {
|
||||
let dashed = self.dashed;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
|
||||
@@ -70,7 +70,6 @@ struct StateInner {
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
measuring_behavior: ListMeasuringBehavior,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -104,26 +103,6 @@ pub enum ListSizingBehavior {
|
||||
Auto,
|
||||
}
|
||||
|
||||
/// The measuring behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListMeasuringBehavior {
|
||||
/// Measure all items in the list.
|
||||
/// Note: This can be expensive for the first frame in a large list.
|
||||
Measure(bool),
|
||||
/// Only measure visible items
|
||||
#[default]
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl ListMeasuringBehavior {
|
||||
fn reset(&mut self) {
|
||||
match self {
|
||||
ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
|
||||
ListMeasuringBehavior::Visible => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The horizontal sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListHorizontalSizingBehavior {
|
||||
@@ -224,20 +203,11 @@ impl ListState {
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
measuring_behavior: ListMeasuringBehavior::default(),
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
}
|
||||
|
||||
/// Set the list to measure all items in the list in the first layout phase.
|
||||
///
|
||||
/// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements.
|
||||
pub fn measure_all(self) -> Self {
|
||||
self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
|
||||
self
|
||||
}
|
||||
|
||||
/// Reset this instantiation of the list state.
|
||||
///
|
||||
/// Note that this will cause scroll events to be dropped until the next paint.
|
||||
@@ -245,7 +215,6 @@ impl ListState {
|
||||
let old_count = {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.measuring_behavior.reset();
|
||||
state.logical_scroll_top = None;
|
||||
state.scrollbar_drag_start_height = None;
|
||||
state.items.summary().count
|
||||
@@ -555,48 +524,6 @@ impl StateInner {
|
||||
cursor.start().height + logical_scroll_top.offset_in_item
|
||||
}
|
||||
|
||||
fn layout_all_items(
|
||||
&mut self,
|
||||
available_width: Pixels,
|
||||
render_item: &mut RenderItemFn,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match &mut self.measuring_behavior {
|
||||
ListMeasuringBehavior::Visible => {
|
||||
return;
|
||||
}
|
||||
ListMeasuringBehavior::Measure(has_measured) => {
|
||||
if *has_measured {
|
||||
return;
|
||||
}
|
||||
*has_measured = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut cursor = self.items.cursor::<Count>(());
|
||||
let available_item_space = size(
|
||||
AvailableSpace::Definite(available_width),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
|
||||
let mut measured_items = Vec::default();
|
||||
|
||||
for (ix, item) in cursor.enumerate() {
|
||||
let size = item.size().unwrap_or_else(|| {
|
||||
let mut element = render_item(ix, window, cx);
|
||||
element.layout_as_root(available_item_space, window, cx)
|
||||
});
|
||||
|
||||
measured_items.push(ListItem::Measured {
|
||||
size,
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
self.items = SumTree::from_iter(measured_items, ());
|
||||
}
|
||||
|
||||
fn layout_items(
|
||||
&mut self,
|
||||
available_width: Option<Pixels>,
|
||||
@@ -784,13 +711,6 @@ impl StateInner {
|
||||
cx: &mut App,
|
||||
) -> Result<LayoutItemsResponse, ListOffset> {
|
||||
window.transact(|window| {
|
||||
match self.measuring_behavior {
|
||||
ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
|
||||
self.layout_all_items(bounds.size.width, render_item, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut layout_response = self.layout_items(
|
||||
Some(bounds.size.width),
|
||||
bounds.size.height,
|
||||
|
||||
@@ -530,18 +530,8 @@ impl WindowsWindowInner {
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_amount = match modifiers.shift {
|
||||
true => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars
|
||||
}
|
||||
false => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_lines
|
||||
}
|
||||
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
|
||||
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -584,11 +574,7 @@ impl WindowsWindowInner {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_chars = self
|
||||
.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars;
|
||||
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
|
||||
let wheel_distance =
|
||||
@@ -721,8 +707,11 @@ impl WindowsWindowInner {
|
||||
// used by Chrome. However, it may result in one row of pixels being obscured
|
||||
// in our client area. But as Chrome says, "there seems to be no better solution."
|
||||
if is_maximized
|
||||
&& let Some(ref taskbar_position) =
|
||||
self.system_settings.borrow().auto_hide_taskbar_position
|
||||
&& let Some(ref taskbar_position) = self
|
||||
.state
|
||||
.borrow()
|
||||
.system_settings
|
||||
.auto_hide_taskbar_position
|
||||
{
|
||||
// For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
|
||||
// so the window isn't treated as a "fullscreen app", which would cause
|
||||
@@ -1112,11 +1101,9 @@ impl WindowsWindowInner {
|
||||
if wparam.0 != 0 {
|
||||
let mut lock = self.state.borrow_mut();
|
||||
let display = lock.display;
|
||||
lock.system_settings.update(display, wparam.0);
|
||||
lock.click_state.system_update(wparam.0);
|
||||
lock.border_offset.update(handle).log_err();
|
||||
// system settings may emit a window message which wants to take the refcell lock, so drop it
|
||||
drop(lock);
|
||||
self.system_settings.borrow_mut().update(display, wparam.0);
|
||||
} else {
|
||||
self.handle_system_theme_changed(handle, lparam)?;
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ pub struct WindowsWindowState {
|
||||
pub renderer: DirectXRenderer,
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
pub current_cursor: Option<HCURSOR>,
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
@@ -65,7 +66,6 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(super) this: Weak<Self>,
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
pub(crate) state: RefCell<WindowsWindowState>,
|
||||
pub(crate) system_settings: RefCell<WindowsSystemSettings>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) hide_title_bar: bool,
|
||||
pub(crate) is_movable: bool,
|
||||
@@ -115,6 +115,7 @@ impl WindowsWindowState {
|
||||
let system_key_handled = false;
|
||||
let hovered = false;
|
||||
let click_state = ClickState::new();
|
||||
let system_settings = WindowsSystemSettings::new(display);
|
||||
let nc_button_pressed = None;
|
||||
let fullscreen = None;
|
||||
let initial_placement = None;
|
||||
@@ -137,6 +138,7 @@ impl WindowsWindowState {
|
||||
hovered,
|
||||
renderer,
|
||||
click_state,
|
||||
system_settings,
|
||||
current_cursor,
|
||||
nc_button_pressed,
|
||||
display,
|
||||
@@ -229,7 +231,6 @@ impl WindowsWindowInner {
|
||||
validation_number: context.validation_number,
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
platform_window_handle: context.platform_window_handle,
|
||||
system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -643,12 +644,10 @@ impl PlatformWindow for WindowsWindow {
|
||||
let mut btn_encoded = Vec::new();
|
||||
for (index, btn) in answers.iter().enumerate() {
|
||||
let encoded = HSTRING::from(btn.label().as_ref());
|
||||
let button_id = match btn {
|
||||
PromptButton::Ok(_) => IDOK.0,
|
||||
PromptButton::Cancel(_) => IDCANCEL.0,
|
||||
// the first few low integer values are reserved for known buttons
|
||||
// so for simplicity we just go backwards from -1
|
||||
PromptButton::Other(_) => -(index as i32) - 1,
|
||||
let button_id = if btn.is_cancel() {
|
||||
IDCANCEL.0
|
||||
} else {
|
||||
index as i32 - 100
|
||||
};
|
||||
button_id_map.push(button_id);
|
||||
buttons.push(TASKDIALOG_BUTTON {
|
||||
@@ -666,11 +665,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
.context("unable to create task dialog")
|
||||
.log_err();
|
||||
|
||||
if let Some(clicked) =
|
||||
button_id_map.iter().position(|&button_id| button_id == res)
|
||||
{
|
||||
let _ = done_tx.send(clicked);
|
||||
}
|
||||
let clicked = button_id_map
|
||||
.iter()
|
||||
.position(|&button_id| button_id == res)
|
||||
.unwrap();
|
||||
let _ = done_tx.send(clicked);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -403,7 +403,13 @@ impl Default for TextStyle {
|
||||
TextStyle {
|
||||
color: black(),
|
||||
// todo(linux) make this configurable or choose better default
|
||||
font_family: ".SystemUIFont".into(),
|
||||
font_family: if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
"FreeMono".into()
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Segoe UI".into()
|
||||
} else {
|
||||
"Helvetica".into()
|
||||
},
|
||||
font_features: FontFeatures::default(),
|
||||
font_fallbacks: None,
|
||||
font_size: rems(1.).into(),
|
||||
|
||||
@@ -75,13 +75,11 @@ impl TextSystem {
|
||||
font(".ZedMono"),
|
||||
font(".ZedSans"),
|
||||
font("Helvetica"),
|
||||
font("Segoe UI"), // Windows
|
||||
font("Ubuntu"), // Gnome (Ubuntu)
|
||||
font("Adwaita Sans"), // Gnome 47
|
||||
font("Cantarell"), // Gnome
|
||||
font("Noto Sans"), // KDE
|
||||
font("DejaVu Sans"),
|
||||
font("Arial"), // macOS, Windows
|
||||
font("Segoe UI"), // Windows
|
||||
font("Cantarell"), // Gnome
|
||||
font("Ubuntu"), // Gnome (Ubuntu)
|
||||
font("Noto Sans"), // KDE
|
||||
font("DejaVu Sans")
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ use gpui::{
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{CompletionDisplayOptions, Project};
|
||||
use settings::{
|
||||
BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,
|
||||
};
|
||||
use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
|
||||
use ui::{
|
||||
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
|
||||
Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section,
|
||||
@@ -1200,12 +1198,13 @@ impl KeymapEditor {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||
self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
|
||||
self.table_interaction_state.read(cx).scroll_offset(),
|
||||
));
|
||||
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||
cx.spawn(async move |_, _| {
|
||||
remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
|
||||
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
@@ -2289,6 +2288,7 @@ impl KeybindingEditorModal {
|
||||
fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
|
||||
let existing_keybind = self.editing_keybind.clone();
|
||||
let fs = self.fs.clone();
|
||||
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||
|
||||
let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?;
|
||||
new_keystrokes
|
||||
@@ -2367,6 +2367,7 @@ impl KeybindingEditorModal {
|
||||
&action_mapping,
|
||||
new_action_args.as_deref(),
|
||||
&fs,
|
||||
tab_size,
|
||||
keyboard_mapper.as_ref(),
|
||||
)
|
||||
.await
|
||||
@@ -3018,14 +3019,13 @@ async fn save_keybinding_update(
|
||||
action_mapping: &ActionMapping,
|
||||
new_args: Option<&str>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
.context("Failed to load keymap file")?;
|
||||
|
||||
let tab_size = infer_json_indent_size(&keymap_contents);
|
||||
|
||||
let existing_keystrokes = existing.keystrokes().unwrap_or_default();
|
||||
let existing_context = existing.context().and_then(KeybindContextString::local_str);
|
||||
let existing_args = existing
|
||||
@@ -3089,6 +3089,7 @@ async fn save_keybinding_update(
|
||||
async fn remove_keybinding(
|
||||
existing: ProcessedBinding,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(keystrokes) = existing.keystrokes() else {
|
||||
@@ -3097,7 +3098,6 @@ async fn remove_keybinding(
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
.context("Failed to load keymap file")?;
|
||||
let tab_size = infer_json_indent_size(&keymap_contents);
|
||||
|
||||
let operation = settings::KeybindUpdateOperation::Remove {
|
||||
target: settings::KeybindUpdateTarget {
|
||||
|
||||
@@ -810,11 +810,15 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
|
||||
};
|
||||
let request = into_open_ai(
|
||||
request,
|
||||
&self.model.id.0,
|
||||
self.model.supports_parallel_tool_calls,
|
||||
true,
|
||||
model.id(),
|
||||
model.supports_parallel_tool_calls(),
|
||||
model.supports_prompt_cache_key(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@@ -856,11 +860,15 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::XAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match x_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
|
||||
};
|
||||
let request = into_open_ai(
|
||||
request,
|
||||
&self.model.id.0,
|
||||
self.model.supports_parallel_tool_calls,
|
||||
false,
|
||||
model.id(),
|
||||
model.supports_parallel_tool_calls(),
|
||||
model.supports_prompt_cache_key(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -222,7 +222,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Some((lsp::CompletionItemKind::MODULE, detail)) => {
|
||||
let text = format!("{label} {detail}");
|
||||
let source = Rope::from(format!("import {text}").as_str());
|
||||
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
|
||||
let runs = language.highlight_text(&source, 7..7 + text.len());
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
.as_deref()
|
||||
@@ -242,7 +242,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
|
||||
language.highlight_text(&source, 4..4 + text.len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -259,7 +259,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -276,7 +276,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -294,7 +294,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
|
||||
language.highlight_text(&source, 16..16 + text.len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
@@ -312,7 +312,7 @@ impl LspAdapter for GoLspAdapter {
|
||||
let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
|
||||
let runs = adjust_runs(
|
||||
name_offset,
|
||||
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
|
||||
language.highlight_text(&source, 5..5 + text.len()),
|
||||
);
|
||||
let filter_range = completion
|
||||
.filter_text
|
||||
|
||||
@@ -61,7 +61,6 @@ pub struct RenderContext {
|
||||
syntax_theme: Arc<SyntaxTheme>,
|
||||
indent: usize,
|
||||
checkbox_clicked_callback: Option<CheckboxClickedCallback>,
|
||||
is_last_child: bool,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
@@ -95,7 +94,6 @@ impl RenderContext {
|
||||
code_block_background_color: theme.colors().surface_background,
|
||||
code_span_background_color: theme.colors().editor_document_highlight_read_background,
|
||||
checkbox_clicked_callback: None,
|
||||
is_last_child: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,25 +135,12 @@ impl RenderContext {
|
||||
/// We give padding between "This is a block quote."
|
||||
/// and "And this is the next paragraph."
|
||||
fn with_common_p(&self, element: Div) -> Div {
|
||||
if self.indent > 0 && !self.is_last_child {
|
||||
if self.indent > 0 {
|
||||
element.pb(self.scaled_rems(0.75))
|
||||
} else {
|
||||
element
|
||||
}
|
||||
}
|
||||
|
||||
/// The is used to indicate that the current element is the last child or not of its parent.
|
||||
///
|
||||
/// Then we can avoid adding padding to the bottom of the last child.
|
||||
fn with_last_child<R>(&mut self, is_last: bool, render: R) -> AnyElement
|
||||
where
|
||||
R: FnOnce(&mut Self) -> AnyElement,
|
||||
{
|
||||
self.is_last_child = is_last;
|
||||
let element = render(self);
|
||||
self.is_last_child = false;
|
||||
element
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_parsed_markdown(
|
||||
@@ -600,12 +585,7 @@ fn render_markdown_block_quote(
|
||||
let children: Vec<AnyElement> = parsed
|
||||
.children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, child)| {
|
||||
cx.with_last_child(ix + 1 == parsed.children.len(), |cx| {
|
||||
render_markdown_block(child, cx)
|
||||
})
|
||||
})
|
||||
.map(|child| render_markdown_block(child, cx))
|
||||
.collect();
|
||||
|
||||
cx.indent -= 1;
|
||||
|
||||
@@ -103,7 +103,7 @@ pub(crate) mod m_2025_07_08 {
|
||||
pub(crate) mod m_2025_10_01 {
|
||||
mod settings;
|
||||
|
||||
pub(crate) use settings::flatten_code_actions_formatters;
|
||||
pub(crate) use settings::SETTINGS_PATTERNS;
|
||||
}
|
||||
|
||||
pub(crate) mod m_2025_10_02 {
|
||||
|
||||
@@ -1,74 +1,109 @@
|
||||
use crate::patterns::migrate_language_setting;
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use std::ops::Range;
|
||||
use tree_sitter::{Query, QueryMatch};
|
||||
|
||||
pub fn flatten_code_actions_formatters(value: &mut Value) -> Result<()> {
|
||||
migrate_language_setting(value, |value, _path| {
|
||||
let Some(obj) = value.as_object_mut() else {
|
||||
return Ok(());
|
||||
use crate::MigrationPatterns;
|
||||
|
||||
pub const SETTINGS_PATTERNS: MigrationPatterns =
|
||||
&[(FORMATTER_PATTERN, migrate_code_action_formatters)];
|
||||
|
||||
const FORMATTER_PATTERN: &str = r#"
|
||||
(object
|
||||
(pair
|
||||
key: (string (string_content) @formatter) (#any-of? @formatter "formatter" "format_on_save")
|
||||
value: [
|
||||
(array
|
||||
(object
|
||||
(pair
|
||||
key: (string (string_content) @code-actions-key) (#eq? @code-actions-key "code_actions")
|
||||
value: (object
|
||||
((pair) @code-action ","?)*
|
||||
)
|
||||
)
|
||||
) @code-actions-obj
|
||||
) @formatter-array
|
||||
(object
|
||||
(pair
|
||||
key: (string (string_content) @code-actions-key) (#eq? @code-actions-key "code_actions")
|
||||
value: (object
|
||||
((pair) @code-action ","?)*
|
||||
)
|
||||
)
|
||||
) @code-actions-obj
|
||||
]
|
||||
)
|
||||
)
|
||||
"#;
|
||||
|
||||
pub fn migrate_code_action_formatters(
|
||||
contents: &str,
|
||||
mat: &QueryMatch,
|
||||
query: &Query,
|
||||
) -> Option<(Range<usize>, String)> {
|
||||
let code_actions_obj_ix = query.capture_index_for_name("code-actions-obj")?;
|
||||
let code_actions_obj_node = mat.nodes_for_capture_index(code_actions_obj_ix).next()?;
|
||||
|
||||
let mut code_actions = vec![];
|
||||
|
||||
let code_actions_ix = query.capture_index_for_name("code-action")?;
|
||||
for code_action_node in mat.nodes_for_capture_index(code_actions_ix) {
|
||||
let Some(enabled) = code_action_node
|
||||
.child_by_field_name("value")
|
||||
.map(|n| n.kind() != "false")
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for key in ["formatter", "format_on_save"] {
|
||||
let Some(formatter) = obj.get_mut(key) else {
|
||||
continue;
|
||||
};
|
||||
let new_formatter = match formatter {
|
||||
Value::Array(arr) => {
|
||||
let mut new_arr = Vec::new();
|
||||
let mut found_code_actions = false;
|
||||
for item in arr {
|
||||
let Some(obj) = item.as_object() else {
|
||||
new_arr.push(item.clone());
|
||||
continue;
|
||||
};
|
||||
let code_actions_obj = obj
|
||||
.get("code_actions")
|
||||
.and_then(|code_actions| code_actions.as_object());
|
||||
let Some(code_actions) = code_actions_obj else {
|
||||
new_arr.push(item.clone());
|
||||
continue;
|
||||
};
|
||||
found_code_actions = true;
|
||||
for (name, enabled) in code_actions {
|
||||
if !enabled.as_bool().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
new_arr.push(serde_json::json!({
|
||||
"code_action": name
|
||||
}));
|
||||
}
|
||||
}
|
||||
if !found_code_actions {
|
||||
continue;
|
||||
}
|
||||
Value::Array(new_arr)
|
||||
}
|
||||
Value::Object(obj) => {
|
||||
let mut new_arr = Vec::new();
|
||||
let code_actions_obj = obj
|
||||
.get("code_actions")
|
||||
.and_then(|code_actions| code_actions.as_object());
|
||||
let Some(code_actions) = code_actions_obj else {
|
||||
continue;
|
||||
};
|
||||
for (name, enabled) in code_actions {
|
||||
if !enabled.as_bool().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
new_arr.push(serde_json::json!({
|
||||
"code_action": name
|
||||
}));
|
||||
}
|
||||
if new_arr.len() == 1 {
|
||||
new_arr.pop().unwrap()
|
||||
} else {
|
||||
Value::Array(new_arr)
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
obj.insert(key.to_string(), new_formatter);
|
||||
if !enabled {
|
||||
continue;
|
||||
}
|
||||
return Ok(());
|
||||
})
|
||||
let Some(name) = code_action_node
|
||||
.child_by_field_name("key")
|
||||
.and_then(|n| n.child(1))
|
||||
.map(|n| &contents[n.byte_range()])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
code_actions.push(name);
|
||||
}
|
||||
|
||||
let indent = query
|
||||
.capture_index_for_name("formatter")
|
||||
.and_then(|ix| mat.nodes_for_capture_index(ix).next())
|
||||
.map(|node| node.start_position().column + 1)
|
||||
.unwrap_or(2);
|
||||
|
||||
let mut code_actions_str = code_actions
|
||||
.into_iter()
|
||||
.map(|code_action| format!(r#"{{ "code_action": "{}" }}"#, code_action))
|
||||
.collect::<Vec<_>>()
|
||||
.join(&format!(",\n{}", " ".repeat(indent)));
|
||||
let is_array = query
|
||||
.capture_index_for_name("formatter-array")
|
||||
.map(|ix| mat.nodes_for_capture_index(ix).count() > 0)
|
||||
.unwrap_or(false);
|
||||
if !is_array {
|
||||
code_actions_str.insert_str(0, &" ".repeat(indent));
|
||||
code_actions_str.insert_str(0, "[\n");
|
||||
code_actions_str.push('\n');
|
||||
code_actions_str.push_str(&" ".repeat(indent.saturating_sub(2)));
|
||||
code_actions_str.push_str("]");
|
||||
}
|
||||
let mut replace_range = code_actions_obj_node.byte_range();
|
||||
if is_array && code_actions_str.is_empty() {
|
||||
let mut cursor = code_actions_obj_node.parent().unwrap().walk();
|
||||
cursor.goto_first_child();
|
||||
while cursor.node().id() != code_actions_obj_node.id() && cursor.goto_next_sibling() {}
|
||||
while cursor.goto_next_sibling()
|
||||
&& (cursor.node().is_extra()
|
||||
|| cursor.node().is_missing()
|
||||
|| cursor.node().kind() == "comment")
|
||||
{}
|
||||
if cursor.node().kind() == "," {
|
||||
// found comma, delete up to next node
|
||||
while cursor.goto_next_sibling()
|
||||
&& (cursor.node().is_extra() || cursor.node().is_missing())
|
||||
{}
|
||||
replace_range.end = cursor.node().range().start_byte;
|
||||
}
|
||||
}
|
||||
Some((replace_range, code_actions_str))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::patterns::migrate_language_setting;
|
||||
|
||||
pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> {
|
||||
migrate_language_setting(value, remove_formatters_on_save_inner)
|
||||
remove_formatters_on_save_inner(value, &[])?;
|
||||
let languages = value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.get_mut("languages"))
|
||||
.and_then(|languages| languages.as_object_mut());
|
||||
if let Some(languages) = languages {
|
||||
for (language_name, language) in languages.iter_mut() {
|
||||
let path = vec!["languages", language_name];
|
||||
remove_formatters_on_save_inner(language, &path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_formatters_on_save_inner(value: &mut Value, path: &[&str]) -> Result<()> {
|
||||
|
||||
@@ -2,61 +2,26 @@ use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
|
||||
let defaults =
|
||||
settings::parse_json_with_comments::<Value>(settings::default_settings().as_ref()).unwrap();
|
||||
let default_root_formatter = defaults.get("formatter");
|
||||
|
||||
let root_code_actions =
|
||||
remove_code_actions_on_format_inner(value, default_root_formatter, &[], &[])?;
|
||||
let user_default_formatter = value.get("formatter").cloned();
|
||||
|
||||
let user_languages = value
|
||||
remove_code_actions_on_format_inner(value, &[])?;
|
||||
let languages = value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.get_mut("languages"))
|
||||
.and_then(|languages| languages.as_object_mut());
|
||||
let default_languages = defaults
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("languages"))
|
||||
.and_then(|languages| languages.as_object());
|
||||
|
||||
if let Some(languages) = user_languages {
|
||||
if let Some(languages) = languages {
|
||||
for (language_name, language) in languages.iter_mut() {
|
||||
let path = vec!["languages", language_name];
|
||||
let language_default_formatter = default_languages
|
||||
.and_then(|langs| langs.get(language_name))
|
||||
.and_then(|lang| lang.get("formatter"));
|
||||
|
||||
// override order:
|
||||
// 4. root from defaults
|
||||
// 3. language from defaults
|
||||
// 2. root from user
|
||||
// 1. language from user
|
||||
let default_formatter_for_language = user_default_formatter
|
||||
.as_ref()
|
||||
.or(language_default_formatter)
|
||||
.or(default_root_formatter);
|
||||
remove_code_actions_on_format_inner(
|
||||
language,
|
||||
default_formatter_for_language,
|
||||
&root_code_actions,
|
||||
&path,
|
||||
)?;
|
||||
remove_code_actions_on_format_inner(language, &path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_code_actions_on_format_inner(
|
||||
value: &mut Value,
|
||||
default_formatters: Option<&Value>,
|
||||
parent_code_actions: &[String],
|
||||
path: &[&str],
|
||||
) -> Result<Vec<String>> {
|
||||
fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
|
||||
let Some(obj) = value.as_object_mut() else {
|
||||
return Ok(vec![]);
|
||||
return Ok(());
|
||||
};
|
||||
let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else {
|
||||
return Ok(vec![]);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
fn fmt_path(path: &[&str], key: &str) -> String {
|
||||
@@ -79,7 +44,6 @@ fn remove_code_actions_on_format_inner(
|
||||
}
|
||||
code_actions.push(code_action.clone());
|
||||
}
|
||||
code_actions.extend(parent_code_actions.iter().cloned());
|
||||
|
||||
let mut formatter_array = vec![];
|
||||
if let Some(formatter) = obj.get("formatter") {
|
||||
@@ -88,18 +52,12 @@ fn remove_code_actions_on_format_inner(
|
||||
} else {
|
||||
formatter_array.insert(0, formatter.clone());
|
||||
}
|
||||
} else if let Some(formatter) = default_formatters {
|
||||
if let Some(array) = formatter.as_array() {
|
||||
formatter_array = array.clone();
|
||||
} else {
|
||||
formatter_array.push(formatter.clone());
|
||||
}
|
||||
};
|
||||
let found_code_actions = !code_actions.is_empty();
|
||||
formatter_array.splice(
|
||||
0..0,
|
||||
code_actions
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|code_action| serde_json::json!({"code_action": code_action})),
|
||||
);
|
||||
|
||||
@@ -108,5 +66,5 @@ fn remove_code_actions_on_format_inner(
|
||||
obj.insert("formatter".to_string(), Value::Array(formatter_array));
|
||||
}
|
||||
|
||||
Ok(code_actions)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,5 +10,4 @@ pub(crate) use settings::{
|
||||
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
|
||||
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
|
||||
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
|
||||
migrate_language_setting,
|
||||
};
|
||||
|
||||
@@ -108,24 +108,3 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
|
||||
(#eq? @agent1 "agent")
|
||||
(#eq? @agent2 "agent")
|
||||
)"#;
|
||||
|
||||
/// Migrate language settings,
|
||||
/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
|
||||
/// Fails early if `migrate_fn` returns an error at any point
|
||||
pub fn migrate_language_setting(
|
||||
value: &mut serde_json::Value,
|
||||
migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
migrate_fn(value, &[])?;
|
||||
let languages = value
|
||||
.as_object_mut()
|
||||
.and_then(|obj| obj.get_mut("languages"))
|
||||
.and_then(|languages| languages.as_object_mut());
|
||||
if let Some(languages) = languages {
|
||||
for (language_name, language) in languages.iter_mut() {
|
||||
let path = vec!["languages", language_name];
|
||||
migrate_fn(language, &path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{
|
||||
AnyView, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Point,
|
||||
Subscription,
|
||||
AnyView, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
};
|
||||
use ui::{
|
||||
FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*,
|
||||
App, ButtonCommon, FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle,
|
||||
PopoverTrigger, RenderOnce, Window, px,
|
||||
};
|
||||
|
||||
use crate::{Picker, PickerDelegate};
|
||||
@@ -19,7 +19,6 @@ where
|
||||
tooltip: TT,
|
||||
handle: Option<PopoverMenuHandle<Picker<P>>>,
|
||||
anchor: Corner,
|
||||
offset: Option<Point<Pixels>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -44,10 +43,6 @@ where
|
||||
trigger,
|
||||
tooltip,
|
||||
handle: None,
|
||||
offset: Some(Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
}),
|
||||
anchor,
|
||||
}
|
||||
}
|
||||
@@ -56,11 +51,6 @@ where
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
|
||||
self.offset = Some(offset);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, TT, P> EventEmitter<DismissEvent> for PickerPopoverMenu<T, TT, P>
|
||||
@@ -96,6 +86,9 @@ where
|
||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||
.anchor(self.anchor)
|
||||
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
|
||||
.when_some(self.offset, |menu, offset| menu.offset(offset))
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,6 +577,7 @@ fn get_or_npm_install_builtin_agent(
|
||||
package_name: SharedString,
|
||||
entrypoint_path: PathBuf,
|
||||
minimum_version: Option<semver::Version>,
|
||||
channel: &'static str,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -648,10 +649,12 @@ fn get_or_npm_install_builtin_agent(
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
// TODO remove the filter
|
||||
let latest_version = node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.ok();
|
||||
.ok()
|
||||
.filter(|_| channel == "latest");
|
||||
if let Some(latest_version) = latest_version
|
||||
&& &latest_version != &file_name.to_string_lossy()
|
||||
{
|
||||
@@ -660,6 +663,7 @@ fn get_or_npm_install_builtin_agent(
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name.clone(),
|
||||
channel,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
@@ -683,6 +687,7 @@ fn get_or_npm_install_builtin_agent(
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name.clone(),
|
||||
channel,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
@@ -731,13 +736,14 @@ async fn download_latest_version(
|
||||
dir: PathBuf,
|
||||
node_runtime: NodeRuntime,
|
||||
package_name: SharedString,
|
||||
channel: &'static str,
|
||||
) -> Result<String> {
|
||||
log::debug!("downloading latest version of {package_name}");
|
||||
|
||||
let tmp_dir = tempfile::tempdir_in(&dir)?;
|
||||
|
||||
node_runtime
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, channel)])
|
||||
.await?;
|
||||
|
||||
let version = node_runtime
|
||||
@@ -880,12 +886,17 @@ impl ExternalAgentServer for LocalGemini {
|
||||
GEMINI_NAME.into(),
|
||||
"@google/gemini-cli".into(),
|
||||
"node_modules/@google/gemini-cli/dist/index.js".into(),
|
||||
// TODO remove these windows-specific workarounds once v0.9.0 stable is released
|
||||
if cfg!(windows) {
|
||||
// v0.8.x on Windows has a bug that causes the initialize request to hang forever
|
||||
Some("0.9.0".parse().unwrap())
|
||||
Some("0.9.0-preview.4".parse().unwrap())
|
||||
} else {
|
||||
Some("0.2.1".parse().unwrap())
|
||||
},
|
||||
if cfg!(windows) {
|
||||
"0.9.0-preview.4"
|
||||
} else {
|
||||
"latest"
|
||||
},
|
||||
status_tx,
|
||||
new_version_available_tx,
|
||||
fs,
|
||||
@@ -969,6 +980,7 @@ impl ExternalAgentServer for LocalClaudeCode {
|
||||
"@zed-industries/claude-code-acp".into(),
|
||||
"node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
|
||||
Some("0.5.2".parse().unwrap()),
|
||||
"latest",
|
||||
status_tx,
|
||||
new_version_available_tx,
|
||||
fs,
|
||||
|
||||
@@ -263,7 +263,7 @@ async fn load_shell_environment(
|
||||
.into_iter()
|
||||
.collect();
|
||||
(Some(fake_env), None)
|
||||
} else if cfg!(target_os = "windows") {
|
||||
} else if cfg!(target_os = "windows",) {
|
||||
let (shell, args) = shell.program_and_args();
|
||||
let envs = match shell_env::capture(shell, args, dir).await {
|
||||
Ok(envs) => envs,
|
||||
|
||||
@@ -1710,7 +1710,12 @@ impl LocalLspStore {
|
||||
formatting_transaction_id,
|
||||
cx,
|
||||
|buffer, cx| {
|
||||
zlog::info!(
|
||||
"Applying edits {edits:?}. Content: {:?}",
|
||||
buffer.text()
|
||||
);
|
||||
buffer.edit(edits, None, cx);
|
||||
zlog::info!("Applied edits. New Content: {:?}", buffer.text());
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -2698,10 +2698,8 @@ impl ProjectPanel {
|
||||
});
|
||||
|
||||
if item_count == 1 {
|
||||
// open entry if not dir, setting is enabled, and only focus if rename is not pending
|
||||
if !entry.is_dir()
|
||||
&& ProjectPanelSettings::get_global(cx).open_file_on_paste
|
||||
{
|
||||
// open entry if not dir, and only focus if rename is not pending
|
||||
if !entry.is_dir() {
|
||||
project_panel.open_entry(
|
||||
entry.id,
|
||||
disambiguation_range.is_none(),
|
||||
|
||||
@@ -32,7 +32,6 @@ pub struct ProjectPanelSettings {
|
||||
pub hide_root: bool,
|
||||
pub hide_hidden: bool,
|
||||
pub drag_and_drop: bool,
|
||||
pub open_file_on_paste: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -83,7 +82,6 @@ impl Settings for ProjectPanelSettings {
|
||||
hide_root: project_panel.hide_root.unwrap(),
|
||||
hide_hidden: project_panel.hide_hidden.unwrap(),
|
||||
drag_and_drop: project_panel.drag_and_drop.unwrap(),
|
||||
open_file_on_paste: project_panel.open_file_on_paste.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ impl WslRemoteConnection {
|
||||
this.can_exec = this.detect_can_exec(shell).await?;
|
||||
this.platform = this.detect_platform(shell).await?;
|
||||
this.remote_binary_path = Some(
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, shell, cx)
|
||||
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
|
||||
.await?,
|
||||
);
|
||||
log::debug!("Detected WSL environment: {this:#?}");
|
||||
@@ -163,7 +163,6 @@ impl WslRemoteConnection {
|
||||
release_channel: ReleaseChannel,
|
||||
version: SemanticVersion,
|
||||
commit: Option<AppCommitSha>,
|
||||
shell: ShellKind,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Arc<RelPath>> {
|
||||
let version_str = match release_channel {
|
||||
@@ -185,13 +184,9 @@ impl WslRemoteConnection {
|
||||
paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
let parent = parent.display(PathStyle::Posix);
|
||||
if shell == ShellKind::Nushell {
|
||||
self.run_wsl_command("mkdir", &[&parent]).await
|
||||
} else {
|
||||
self.run_wsl_command("mkdir", &["-p", &parent]).await
|
||||
}
|
||||
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
|
||||
self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -566,10 +566,6 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub drag_and_drop: Option<bool>,
|
||||
/// Whether to automatically open files when pasting them in the project panel.
|
||||
///
|
||||
/// Default: true
|
||||
pub open_file_on_paste: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -262,8 +262,8 @@ pub fn replace_value_in_json_text<T: AsRef<str>>(
|
||||
} else {
|
||||
// We don't have the key, construct the nested objects
|
||||
let new_value = construct_json_value(&key_path[depth..], new_value);
|
||||
let indent_prefix_len = tab_size * depth;
|
||||
let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len);
|
||||
let indent_prefix_len = 4 * depth;
|
||||
let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
||||
if depth == 0 {
|
||||
new_val.push('\n');
|
||||
}
|
||||
@@ -628,100 +628,6 @@ pub fn append_top_level_array_value_in_json_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// Infers the indentation size used in JSON text by analyzing the tree structure.
|
||||
/// Returns the detected indent size, or a default of 2 if no indentation is found.
|
||||
pub fn infer_json_indent_size(text: &str) -> usize {
|
||||
const MAX_INDENT_SIZE: usize = 64;
|
||||
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter_json::LANGUAGE.into())
|
||||
.unwrap();
|
||||
|
||||
let Some(syntax_tree) = parser.parse(text, None) else {
|
||||
return 4;
|
||||
};
|
||||
|
||||
let mut cursor = syntax_tree.walk();
|
||||
let mut indent_counts = [0u32; MAX_INDENT_SIZE];
|
||||
|
||||
// Traverse the tree to find indentation patterns
|
||||
fn visit_node(
|
||||
cursor: &mut tree_sitter::TreeCursor,
|
||||
indent_counts: &mut [u32; MAX_INDENT_SIZE],
|
||||
depth: usize,
|
||||
) {
|
||||
if depth >= 3 {
|
||||
return;
|
||||
}
|
||||
let node = cursor.node();
|
||||
let node_kind = node.kind();
|
||||
|
||||
// For objects and arrays, check the indentation of their first content child
|
||||
if matches!(node_kind, "object" | "array") {
|
||||
let container_column = node.start_position().column;
|
||||
let container_row = node.start_position().row;
|
||||
|
||||
if cursor.goto_first_child() {
|
||||
// Skip the opening bracket
|
||||
loop {
|
||||
let child = cursor.node();
|
||||
let child_kind = child.kind();
|
||||
|
||||
// Look for the first actual content (pair for objects, value for arrays)
|
||||
if (node_kind == "object" && child_kind == "pair")
|
||||
|| (node_kind == "array"
|
||||
&& !matches!(child_kind, "[" | "]" | "," | "comment"))
|
||||
{
|
||||
let child_column = child.start_position().column;
|
||||
let child_row = child.start_position().row;
|
||||
|
||||
// Only count if the child is on a different line
|
||||
if child_row > container_row && child_column > container_column {
|
||||
let indent = child_column - container_column;
|
||||
if indent > 0 && indent < MAX_INDENT_SIZE {
|
||||
indent_counts[indent] += 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if !cursor.goto_next_sibling() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cursor.goto_parent();
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse to children
|
||||
if cursor.goto_first_child() {
|
||||
loop {
|
||||
visit_node(cursor, indent_counts, depth + 1);
|
||||
if !cursor.goto_next_sibling() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cursor.goto_parent();
|
||||
}
|
||||
}
|
||||
|
||||
visit_node(&mut cursor, &mut indent_counts, 0);
|
||||
|
||||
// Find the indent size with the highest count
|
||||
let mut max_count = 0;
|
||||
let mut max_indent = 4;
|
||||
|
||||
for (indent, &count) in indent_counts.iter().enumerate() {
|
||||
if count > max_count {
|
||||
max_count = count;
|
||||
max_indent = indent;
|
||||
}
|
||||
}
|
||||
|
||||
if max_count == 0 { 2 } else { max_indent }
|
||||
}
|
||||
|
||||
pub fn to_pretty_json(
|
||||
value: &impl Serialize,
|
||||
indent_size: usize,
|
||||
@@ -2580,69 +2486,4 @@ mod tests {
|
||||
.unindent(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infer_json_indent_size() {
|
||||
let json_2_spaces = r#"{
|
||||
"key1": "value1",
|
||||
"nested": {
|
||||
"key2": "value2",
|
||||
"array": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
assert_eq!(infer_json_indent_size(json_2_spaces), 2);
|
||||
|
||||
let json_4_spaces = r#"{
|
||||
"key1": "value1",
|
||||
"nested": {
|
||||
"key2": "value2",
|
||||
"array": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
assert_eq!(infer_json_indent_size(json_4_spaces), 4);
|
||||
|
||||
let json_8_spaces = r#"{
|
||||
"key1": "value1",
|
||||
"nested": {
|
||||
"key2": "value2"
|
||||
}
|
||||
}"#;
|
||||
assert_eq!(infer_json_indent_size(json_8_spaces), 8);
|
||||
|
||||
let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#;
|
||||
assert_eq!(infer_json_indent_size(json_single_line), 2);
|
||||
|
||||
let json_empty = r#"{}"#;
|
||||
assert_eq!(infer_json_indent_size(json_empty), 2);
|
||||
|
||||
let json_array = r#"[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "second"
|
||||
}
|
||||
]"#;
|
||||
assert_eq!(infer_json_indent_size(json_array), 2);
|
||||
|
||||
let json_mixed = r#"{
|
||||
"a": {
|
||||
"b": {
|
||||
"c": "value"
|
||||
}
|
||||
},
|
||||
"d": "value2"
|
||||
}"#;
|
||||
assert_eq!(infer_json_indent_size(json_mixed), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ pub type EditorconfigProperties = ec4rs::Properties;
|
||||
use crate::{
|
||||
ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
|
||||
LanguageToSettingsMap, SettingsJsonSchemaParams, ThemeName, VsCodeSettings, WorktreeId,
|
||||
infer_json_indent_size,
|
||||
merge_from::MergeFrom,
|
||||
parse_json_with_comments,
|
||||
settings_content::{
|
||||
@@ -638,7 +637,7 @@ impl SettingsStore {
|
||||
|
||||
let mut key_path = Vec::new();
|
||||
let mut edits = Vec::new();
|
||||
let tab_size = infer_json_indent_size(&text);
|
||||
let tab_size = self.json_tab_size();
|
||||
let mut text = text.to_string();
|
||||
update_value_in_json_text(
|
||||
&mut text,
|
||||
@@ -651,6 +650,10 @@ impl SettingsStore {
|
||||
edits
|
||||
}
|
||||
|
||||
pub fn json_tab_size(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
/// Sets the default settings via a JSON string.
|
||||
///
|
||||
/// The string should contain a JSON object with a default value for every setting.
|
||||
@@ -1537,9 +1540,9 @@ mod tests {
|
||||
})
|
||||
},
|
||||
r#"{
|
||||
"tabs": {
|
||||
"git_status": true
|
||||
}
|
||||
"tabs": {
|
||||
"git_status": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
@@ -1554,9 +1557,9 @@ mod tests {
|
||||
.unindent(),
|
||||
|settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true),
|
||||
r#"{
|
||||
"title_bar": {
|
||||
"show_branch_name": true
|
||||
}
|
||||
"title_bar": {
|
||||
"show_branch_name": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
@@ -1581,7 +1584,7 @@ mod tests {
|
||||
.unindent(),
|
||||
r#" { "editor.tabSize": 37 } "#.to_owned(),
|
||||
r#"{
|
||||
"tab_size": 37
|
||||
"tab_size": 37
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
@@ -1634,9 +1637,9 @@ mod tests {
|
||||
.unindent(),
|
||||
r#"{ "workbench.editor.decorations.colors": true }"#.to_owned(),
|
||||
r#"{
|
||||
"tabs": {
|
||||
"git_status": true
|
||||
}
|
||||
"tabs": {
|
||||
"git_status": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
@@ -1652,11 +1655,11 @@ mod tests {
|
||||
.unindent(),
|
||||
r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(),
|
||||
r#"{
|
||||
"buffer_font_fallbacks": [
|
||||
"Consolas",
|
||||
"Courier New"
|
||||
],
|
||||
"buffer_font_family": "Cascadia Code"
|
||||
"buffer_font_fallbacks": [
|
||||
"Consolas",
|
||||
"Courier New"
|
||||
],
|
||||
"buffer_font_family": "Cascadia Code"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
@@ -1692,16 +1695,16 @@ mod tests {
|
||||
.get_or_insert_default()
|
||||
.enabled = Some(true);
|
||||
});
|
||||
pretty_assertions::assert_str_eq!(
|
||||
assert_eq!(
|
||||
actual,
|
||||
r#"{
|
||||
"git": {
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
}
|
||||
"#
|
||||
.unindent()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1723,7 +1723,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
title: language_name,
|
||||
files: USER | LOCAL,
|
||||
render: Arc::new(|this, window, cx| {
|
||||
this.render_sub_page_items(
|
||||
this.render_page_items(
|
||||
language_settings_data()
|
||||
.iter()
|
||||
.chain(non_editor_language_settings_data().iter())
|
||||
@@ -3120,27 +3120,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Open File on Paste",
|
||||
description: "Whether to automatically open files when pasting them in the project panel",
|
||||
field: Box::new(SettingField {
|
||||
pick: |settings_content| {
|
||||
if let Some(project_panel) = &settings_content.project_panel {
|
||||
&project_panel.open_file_on_paste
|
||||
} else {
|
||||
&None
|
||||
}
|
||||
},
|
||||
pick_mut: |settings_content| {
|
||||
&mut settings_content
|
||||
.project_panel
|
||||
.get_or_insert_default()
|
||||
.open_file_on_paste
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Terminal Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Terminal Dock",
|
||||
|
||||
@@ -6,10 +6,10 @@ use editor::{Editor, EditorEvent};
|
||||
use feature_flags::FeatureFlag;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
Action, App, Div, Entity, FocusHandle, Focusable, FontWeight, Global, ListState,
|
||||
ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task, TitlebarOptions,
|
||||
UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions, actions, div, list,
|
||||
point, prelude::*, px, size, uniform_list,
|
||||
Action, App, Div, Entity, FocusHandle, Focusable, FontWeight, Global, ReadGlobal as _,
|
||||
ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window,
|
||||
WindowBounds, WindowHandle, WindowOptions, actions, div, point, prelude::*, px, size,
|
||||
uniform_list,
|
||||
};
|
||||
use heck::ToTitleCase as _;
|
||||
use project::WorktreeId;
|
||||
@@ -536,14 +536,12 @@ pub struct SettingsWindow {
|
||||
filter_table: Vec<Vec<bool>>,
|
||||
has_query: bool,
|
||||
content_handles: Vec<Vec<Entity<NonFocusableHandle>>>,
|
||||
sub_page_scroll_handle: ScrollHandle,
|
||||
page_scroll_handle: ScrollHandle,
|
||||
focus_handle: FocusHandle,
|
||||
navbar_focus_handle: Entity<NonFocusableHandle>,
|
||||
content_focus_handle: Entity<NonFocusableHandle>,
|
||||
files_focus_handle: FocusHandle,
|
||||
search_index: Option<Arc<SearchIndex>>,
|
||||
visible_items: Vec<usize>,
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
struct SearchIndex {
|
||||
@@ -603,7 +601,7 @@ impl SettingsPageItem {
|
||||
fn render(
|
||||
&self,
|
||||
settings_window: &SettingsWindow,
|
||||
item_index: usize,
|
||||
section_header: &'static str,
|
||||
is_last: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
@@ -714,23 +712,7 @@ impl SettingsPageItem {
|
||||
.on_click({
|
||||
let sub_page_link = sub_page_link.clone();
|
||||
cx.listener(move |this, _, _, cx| {
|
||||
let mut section_index = item_index;
|
||||
let current_page = this.current_page();
|
||||
|
||||
while !matches!(
|
||||
current_page.items[section_index],
|
||||
SettingsPageItem::SectionHeader(_)
|
||||
) {
|
||||
section_index -= 1;
|
||||
}
|
||||
|
||||
let SettingsPageItem::SectionHeader(header) =
|
||||
current_page.items[section_index]
|
||||
else {
|
||||
unreachable!("All items always have a section header above them")
|
||||
};
|
||||
|
||||
this.push_sub_page(sub_page_link.clone(), header, cx)
|
||||
this.push_sub_page(sub_page_link.clone(), section_header, cx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -998,10 +980,6 @@ impl SettingsWindow {
|
||||
None
|
||||
};
|
||||
|
||||
// high overdraw value so the list scrollbar len doesn't change too much
|
||||
let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.0)).measure_all();
|
||||
list_state.set_scroll_handler(|_, _, _| {});
|
||||
|
||||
let mut this = Self {
|
||||
title_bar,
|
||||
original_window,
|
||||
@@ -1018,7 +996,7 @@ impl SettingsWindow {
|
||||
filter_table: vec![],
|
||||
has_query: false,
|
||||
content_handles: vec![],
|
||||
sub_page_scroll_handle: ScrollHandle::new(),
|
||||
page_scroll_handle: ScrollHandle::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
navbar_focus_handle: NonFocusableHandle::new(
|
||||
NAVBAR_CONTAINER_TAB_INDEX,
|
||||
@@ -1038,8 +1016,6 @@ impl SettingsWindow {
|
||||
.tab_index(HEADER_CONTAINER_TAB_INDEX)
|
||||
.tab_stop(false),
|
||||
search_index: None,
|
||||
visible_items: Vec::default(),
|
||||
list_state,
|
||||
};
|
||||
|
||||
this.fetch_files(window, cx);
|
||||
@@ -1209,7 +1185,6 @@ impl SettingsWindow {
|
||||
}
|
||||
self.has_query = false;
|
||||
self.filter_matches_to_file();
|
||||
self.reset_list_state();
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -1239,7 +1214,6 @@ impl SettingsWindow {
|
||||
this.has_query = true;
|
||||
this.filter_matches_to_file();
|
||||
this.open_first_nav_page();
|
||||
this.reset_list_state();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1416,18 +1390,6 @@ impl SettingsWindow {
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
fn reset_list_state(&mut self) {
|
||||
// plus one for the title
|
||||
self.visible_items = self.visible_page_items().map(|(index, _)| index).collect();
|
||||
|
||||
if self.visible_items.is_empty() {
|
||||
self.list_state.reset(0);
|
||||
} else {
|
||||
// show page title if page is non empty
|
||||
self.list_state.reset(self.visible_items.len() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ui(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
|
||||
if self.pages.is_empty() {
|
||||
self.pages = page_data::settings_data(cx);
|
||||
@@ -1438,7 +1400,6 @@ impl SettingsWindow {
|
||||
sub_page_stack_mut().clear();
|
||||
// PERF: doesn't have to be rebuilt, can just be filled with true. pages is constant once it is built
|
||||
self.build_filter_table();
|
||||
self.reset_list_state();
|
||||
self.update_matches(cx);
|
||||
|
||||
cx.notify();
|
||||
@@ -1502,17 +1463,7 @@ impl SettingsWindow {
|
||||
if !self.is_nav_entry_visible(navbar_entry) {
|
||||
self.open_first_nav_page();
|
||||
}
|
||||
|
||||
let is_new_page = self.navbar_entries[self.navbar_entry].page_index
|
||||
!= self.navbar_entries[navbar_entry].page_index;
|
||||
self.navbar_entry = navbar_entry;
|
||||
|
||||
// We only need to reset visible items when updating matches
|
||||
// and selecting a new page
|
||||
if is_new_page {
|
||||
self.reset_list_state();
|
||||
}
|
||||
|
||||
sub_page_stack_mut().clear();
|
||||
}
|
||||
|
||||
@@ -1959,8 +1910,7 @@ impl SettingsWindow {
|
||||
if self.navbar_entries[navbar_entry_index].is_root
|
||||
|| !self.is_nav_entry_visible(navbar_entry_index)
|
||||
{
|
||||
self.sub_page_scroll_handle
|
||||
.set_offset(point(px(0.), px(0.)));
|
||||
self.page_scroll_handle.set_offset(point(px(0.), px(0.)));
|
||||
if focus_content {
|
||||
let Some(first_item_index) =
|
||||
self.visible_page_items().next().map(|(index, _)| index)
|
||||
@@ -1981,11 +1931,9 @@ impl SettingsWindow {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.page_scroll_handle
|
||||
.scroll_to_top_of_item(selected_item_index + 1);
|
||||
|
||||
self.list_state.scroll_to(gpui::ListOffset {
|
||||
item_ix: selected_item_index + 1,
|
||||
offset_in_item: px(0.),
|
||||
});
|
||||
if focus_content {
|
||||
self.focus_content_element(entry_item_index, window, cx);
|
||||
} else {
|
||||
@@ -2063,107 +2011,7 @@ impl SettingsWindow {
|
||||
.child(Label::new(last))
|
||||
}
|
||||
|
||||
fn render_page_items(
|
||||
&mut self,
|
||||
page_index: Option<usize>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) -> impl IntoElement {
|
||||
let mut page_content = v_flex().id("settings-ui-page").size_full();
|
||||
|
||||
let has_active_search = !self.search_bar.read(cx).is_empty(cx);
|
||||
let has_no_results = self.visible_items.len() == 0 && has_active_search;
|
||||
|
||||
if has_no_results {
|
||||
let search_query = self.search_bar.read(cx).text(cx);
|
||||
page_content = page_content.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(div().child("No Results"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!("No settings match \"{}\"", search_query)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let items = &self.current_page().items;
|
||||
|
||||
let last_non_header_index = self
|
||||
.visible_items
|
||||
.iter()
|
||||
.map(|index| &items[*index])
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
|
||||
.map(|(index, _)| index);
|
||||
|
||||
let root_nav_label = self
|
||||
.navbar_entries
|
||||
.iter()
|
||||
.find(|entry| entry.is_root && entry.page_index == self.current_page_index())
|
||||
.map(|entry| entry.title);
|
||||
|
||||
let list_content = list(
|
||||
self.list_state.clone(),
|
||||
cx.processor(move |this, index, window, cx| {
|
||||
if index == 0 {
|
||||
return div()
|
||||
.when(sub_page_stack().is_empty(), |this| {
|
||||
this.when_some(root_nav_label, |this, title| {
|
||||
this.child(
|
||||
Label::new(title).size(LabelSize::Large).mt_2().mb_3(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let index = index - 1;
|
||||
let actual_item_index = this.visible_items[index];
|
||||
let item: &SettingsPageItem = &this.current_page().items[actual_item_index];
|
||||
|
||||
let no_bottom_border = this
|
||||
.visible_items
|
||||
.get(index + 1)
|
||||
.map(|item_index| {
|
||||
let item = &this.current_page().items[*item_index];
|
||||
matches!(item, SettingsPageItem::SectionHeader(_))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_last = Some(index) == last_non_header_index;
|
||||
|
||||
v_flex()
|
||||
.id(("settings-page-item", actual_item_index))
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.when_some(page_index, |element, page_index| {
|
||||
element.track_focus(
|
||||
&this.content_handles[page_index][actual_item_index]
|
||||
.focus_handle(cx),
|
||||
)
|
||||
})
|
||||
.child(item.render(
|
||||
this,
|
||||
actual_item_index,
|
||||
no_bottom_border || is_last,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}),
|
||||
);
|
||||
|
||||
page_content = page_content.child(list_content.size_full())
|
||||
}
|
||||
page_content
|
||||
}
|
||||
|
||||
fn render_sub_page_items<'a, Items: Iterator<Item = (usize, &'a SettingsPageItem)>>(
|
||||
fn render_page_items<'a, Items: Iterator<Item = (usize, &'a SettingsPageItem)>>(
|
||||
&self,
|
||||
items: Items,
|
||||
page_index: Option<usize>,
|
||||
@@ -2174,7 +2022,7 @@ impl SettingsWindow {
|
||||
.id("settings-ui-page")
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.sub_page_scroll_handle);
|
||||
.track_scroll(&self.page_scroll_handle);
|
||||
|
||||
let items: Vec<_> = items.collect();
|
||||
let items_len = items.len();
|
||||
@@ -2244,7 +2092,7 @@ impl SettingsWindow {
|
||||
})
|
||||
.child(item.render(
|
||||
self,
|
||||
actual_item_index,
|
||||
section_header.expect("All items rendered after a section header"),
|
||||
no_bottom_border || is_last,
|
||||
window,
|
||||
cx,
|
||||
@@ -2267,7 +2115,12 @@ impl SettingsWindow {
|
||||
page_header = self.render_files_header(window, cx).into_any_element();
|
||||
|
||||
page_content = self
|
||||
.render_page_items(Some(self.current_page_index()), window, cx)
|
||||
.render_page_items(
|
||||
self.visible_page_items(),
|
||||
Some(self.current_page_index()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element();
|
||||
} else {
|
||||
page_header = h_flex()
|
||||
@@ -2290,19 +2143,13 @@ impl SettingsWindow {
|
||||
}
|
||||
|
||||
return v_flex()
|
||||
.id("Settings-ui-page")
|
||||
.flex_1()
|
||||
.pt_6()
|
||||
.pb_8()
|
||||
.px_8()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(page_header)
|
||||
.when(sub_page_stack().is_empty(), |this| {
|
||||
this.vertical_scrollbar_for(self.list_state.clone(), window, cx)
|
||||
})
|
||||
.when(!sub_page_stack().is_empty(), |this| {
|
||||
this.vertical_scrollbar_for(self.sub_page_scroll_handle.clone(), window, cx)
|
||||
})
|
||||
.vertical_scrollbar_for(self.page_scroll_handle.clone(), window, cx)
|
||||
.track_focus(&self.content_focus_handle.focus_handle(cx))
|
||||
.child(
|
||||
div()
|
||||
@@ -2944,7 +2791,7 @@ mod test {
|
||||
has_query: false,
|
||||
content_handles: vec![],
|
||||
search_task: None,
|
||||
sub_page_scroll_handle: ScrollHandle::new(),
|
||||
page_scroll_handle: ScrollHandle::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
navbar_focus_handle: NonFocusableHandle::new(
|
||||
NAVBAR_CONTAINER_TAB_INDEX,
|
||||
@@ -2960,8 +2807,6 @@ mod test {
|
||||
),
|
||||
files_focus_handle: cx.focus_handle(),
|
||||
search_index: None,
|
||||
visible_items: Vec::default(),
|
||||
list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)),
|
||||
};
|
||||
|
||||
settings_window.build_filter_table();
|
||||
|
||||
@@ -189,18 +189,10 @@ impl Render for TitleBar {
|
||||
let status = &*status.borrow();
|
||||
let user = self.user_store.read(cx).current_user();
|
||||
|
||||
let signed_in = user.is_some();
|
||||
|
||||
children.push(
|
||||
h_flex()
|
||||
.map(|this| {
|
||||
if signed_in {
|
||||
this.pr_1p5()
|
||||
} else {
|
||||
this.pr_1()
|
||||
}
|
||||
})
|
||||
.gap_1()
|
||||
.pr_1()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.children(self.render_call_controls(window, cx))
|
||||
.children(self.render_connection_status(status, cx))
|
||||
@@ -763,13 +755,17 @@ impl TitleBar {
|
||||
.map(|this| {
|
||||
if is_signed_in {
|
||||
this.trigger_with_tooltip(
|
||||
ButtonLike::new("user-menu").children(
|
||||
TitleBarSettings::get_global(cx)
|
||||
.show_user_picture
|
||||
.then(|| user_avatar.clone())
|
||||
.flatten()
|
||||
.map(|avatar| Avatar::new(avatar)),
|
||||
),
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_flex().gap_0p5().mr_1().children(
|
||||
TitleBarSettings::get_global(cx)
|
||||
.show_user_picture
|
||||
.then(|| user_avatar.clone())
|
||||
.flatten()
|
||||
.map(|avatar| Avatar::new(avatar)),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle),
|
||||
Tooltip::text("Toggle User Menu"),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -45,7 +45,6 @@ pub fn get_windows_git_bash() -> Option<String> {
|
||||
let git = which::which("git").ok()?;
|
||||
let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe");
|
||||
if git_bash.is_file() {
|
||||
log::info!("Found git-bash at {}", git_bash.display());
|
||||
Some(git_bash.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -177,12 +177,8 @@ async fn capture_windows(
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {}{} --printenv",
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
shell_kind
|
||||
.command_prefix()
|
||||
.map(|prefix| prefix.to_string())
|
||||
.unwrap_or_default(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
|
||||
@@ -1525,6 +1525,29 @@ fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayP
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a point, returns the start of the buffer row that is a given number of
|
||||
/// buffer rows away from the current position.
|
||||
///
|
||||
/// This moves by buffer rows instead of display rows, a distinction that is
|
||||
/// important when soft wrapping is enabled.
|
||||
pub(crate) fn start_of_relative_buffer_row(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
times: isize,
|
||||
) -> DisplayPoint {
|
||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
||||
let target = start.row() as isize + times;
|
||||
let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row());
|
||||
|
||||
map.clip_point(
|
||||
map.fold_point_to_display_point(
|
||||
map.fold_snapshot()
|
||||
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
}
|
||||
|
||||
fn up_down_buffer_rows(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
@@ -2104,7 +2127,7 @@ pub(crate) fn end_of_line(
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
if times > 1 {
|
||||
point = map.start_of_relative_buffer_row(point, times as isize - 1);
|
||||
point = start_of_relative_buffer_row(map, point, times as isize - 1);
|
||||
}
|
||||
if display_lines {
|
||||
map.clip_point(
|
||||
@@ -2709,17 +2732,17 @@ fn sneak_backward(
|
||||
}
|
||||
|
||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let correct_line = map.start_of_relative_buffer_row(point, times as isize);
|
||||
let correct_line = start_of_relative_buffer_row(map, point, times as isize);
|
||||
first_non_whitespace(map, false, correct_line)
|
||||
}
|
||||
|
||||
fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
|
||||
let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
|
||||
first_non_whitespace(map, false, correct_line)
|
||||
}
|
||||
|
||||
fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let correct_line = map.start_of_relative_buffer_row(point, 0);
|
||||
let correct_line = start_of_relative_buffer_row(map, point, 0);
|
||||
right(map, correct_line, times.saturating_sub(1))
|
||||
}
|
||||
|
||||
@@ -2729,7 +2752,7 @@ pub(crate) fn next_line_end(
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
if times > 1 {
|
||||
point = map.start_of_relative_buffer_row(point, times as isize - 1);
|
||||
point = start_of_relative_buffer_row(map, point, times as isize - 1);
|
||||
}
|
||||
end_of_line(map, false, point, 1)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use editor::Editor;
|
||||
use editor::{Anchor, SelectionEffects};
|
||||
use editor::{Bias, ToPoint};
|
||||
use editor::{display_map::ToDisplayPoint, movement};
|
||||
use gpui::{Context, Window, actions};
|
||||
use gpui::{Action, Context, Window, actions};
|
||||
use language::{Point, SelectionGoal};
|
||||
use log::error;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -123,6 +123,8 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::toggle_comments);
|
||||
Vim::action(editor, cx, Vim::paste);
|
||||
Vim::action(editor, cx, Vim::show_location);
|
||||
Vim::action(editor, cx, Vim::go_to_tab);
|
||||
Vim::action(editor, cx, Vim::go_to_previous_tab);
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| {
|
||||
vim.record_current_action(cx);
|
||||
@@ -679,7 +681,7 @@ impl Vim {
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
let previous_line = map.start_of_relative_buffer_row(cursor, -1);
|
||||
let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
|
||||
let insert_point = motion::end_of_line(map, false, previous_line, 1);
|
||||
(insert_point, SelectionGoal::None)
|
||||
});
|
||||
@@ -1012,8 +1014,55 @@ impl Vim {
|
||||
self.switch_mode(Mode::Insert, true, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_tab(&mut self, _: &GoToTab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
|
||||
if let Some(tab_index) = count {
|
||||
// <count>gt goes to tab <count> (1-based).
|
||||
let zero_based_index = tab_index.saturating_sub(1);
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivateItem(zero_based_index).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
// If no count is provided, go to the next tab.
|
||||
window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_previous_tab(
|
||||
&mut self,
|
||||
_: &GoToPreviousTab,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
|
||||
if let Some(count) = count {
|
||||
// gT with count goes back that many tabs with wraparound (not the same as gt!).
|
||||
if let Some(workspace) = self.workspace(window) {
|
||||
let pane = workspace.read(cx).active_pane().read(cx);
|
||||
let item_count = pane.items().count();
|
||||
if item_count > 0 {
|
||||
let current_index = pane.active_item_index();
|
||||
let target_index = (current_index as isize - count as isize)
|
||||
.rem_euclid(item_count as isize)
|
||||
as usize;
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivateItem(target_index).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No count provided, go to the previous tab.
|
||||
window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
|
||||
|
||||
@@ -51,10 +51,7 @@ use vim_mode_setting::HelixModeSetting;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::{self, Pane, Workspace};
|
||||
|
||||
use crate::{
|
||||
normal::{GoToPreviousTab, GoToTab},
|
||||
state::ReplayableAction,
|
||||
};
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
/// Number is used to manage vim's count. Pushing a digit
|
||||
/// multiplies the current value by 10 and adds the digit.
|
||||
@@ -412,46 +409,6 @@ pub fn init(cx: &mut App) {
|
||||
cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx))
|
||||
})
|
||||
});
|
||||
workspace.register_action(|_, _: &GoToTab, window, cx| {
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
|
||||
if let Some(tab_index) = count {
|
||||
// <count>gt goes to tab <count> (1-based).
|
||||
let zero_based_index = tab_index.saturating_sub(1);
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivateItem(zero_based_index).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
// If no count is provided, go to the next tab.
|
||||
window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &GoToPreviousTab, window, cx| {
|
||||
let count = Vim::take_count(cx);
|
||||
Vim::take_forced_motion(cx);
|
||||
|
||||
if let Some(count) = count {
|
||||
// gT with count goes back that many tabs with wraparound (not the same as gt!).
|
||||
let pane = workspace.active_pane().read(cx);
|
||||
let item_count = pane.items().count();
|
||||
if item_count > 0 {
|
||||
let current_index = pane.active_item_index();
|
||||
let target_index = (current_index as isize - count as isize)
|
||||
.rem_euclid(item_count as isize)
|
||||
as usize;
|
||||
window.dispatch_action(
|
||||
workspace::pane::ActivateItem(target_index).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No count provided, go to the previous tab.
|
||||
window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ use workspace::searchable::Direction;
|
||||
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
|
||||
motion::{
|
||||
Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line,
|
||||
start_of_relative_buffer_row,
|
||||
},
|
||||
object::Object,
|
||||
state::{Mark, Mode, Operator},
|
||||
};
|
||||
@@ -403,9 +406,7 @@ impl Vim {
|
||||
// Move to the next or previous buffer row, ensuring that
|
||||
// wrapped lines are handled correctly.
|
||||
let direction = if tail.row() > head.row() { -1 } else { 1 };
|
||||
row = map
|
||||
.start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
|
||||
.row();
|
||||
row = start_of_relative_buffer_row(map, DisplayPoint::new(row, 0), direction).row();
|
||||
}
|
||||
|
||||
s.select(selections);
|
||||
|
||||
@@ -24,7 +24,6 @@ test-support = [
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-lock.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -64,7 +64,7 @@ use std::{
|
||||
use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};
|
||||
use text::{LineEnding, Rope};
|
||||
use util::{
|
||||
ResultExt, debug_panic, maybe,
|
||||
ResultExt, debug_panic,
|
||||
paths::{PathMatcher, PathStyle, SanitizedPath, home_dir},
|
||||
rel_path::RelPath,
|
||||
};
|
||||
@@ -226,7 +226,7 @@ impl Default for WorkDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalSnapshot {
|
||||
snapshot: Snapshot,
|
||||
global_gitignore: Option<Arc<Gitignore>>,
|
||||
@@ -239,7 +239,6 @@ pub struct LocalSnapshot {
|
||||
/// The file handle of the worktree root. `None` if the worktree is a directory.
|
||||
/// (so we can find it after it's been moved)
|
||||
root_file_handle: Option<Arc<dyn fs::FileHandle>>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
struct BackgroundScannerState {
|
||||
@@ -322,6 +321,7 @@ impl DerefMut for LocalSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ScanState {
|
||||
Started,
|
||||
Updated {
|
||||
@@ -402,7 +402,6 @@ impl Worktree {
|
||||
PathStyle::local(),
|
||||
),
|
||||
root_file_handle,
|
||||
executor: cx.background_executor().clone(),
|
||||
};
|
||||
|
||||
let worktree_id = snapshot.id();
|
||||
@@ -1070,7 +1069,7 @@ impl LocalWorktree {
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
next_entry_id,
|
||||
state: async_lock::Mutex::new(BackgroundScannerState {
|
||||
state: Mutex::new(BackgroundScannerState {
|
||||
prev_snapshot: snapshot.snapshot.clone(),
|
||||
snapshot,
|
||||
scanned_dirs: Default::default(),
|
||||
@@ -2443,7 +2442,7 @@ impl LocalSnapshot {
|
||||
log::trace!("insert entry {:?}", entry.path);
|
||||
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
let abs_path = self.absolutize(&entry.path);
|
||||
match self.executor.block(build_gitignore(&abs_path, fs)) {
|
||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||
Ok(ignore) => {
|
||||
self.ignores_by_parent_abs_path
|
||||
.insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
|
||||
@@ -2494,12 +2493,7 @@ impl LocalSnapshot {
|
||||
inodes
|
||||
}
|
||||
|
||||
async fn ignore_stack_for_abs_path(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
is_dir: bool,
|
||||
fs: &dyn Fs,
|
||||
) -> IgnoreStack {
|
||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool, fs: &dyn Fs) -> IgnoreStack {
|
||||
let mut new_ignores = Vec::new();
|
||||
let mut repo_root = None;
|
||||
for (index, ancestor) in abs_path.ancestors().enumerate() {
|
||||
@@ -2510,8 +2504,9 @@ impl LocalSnapshot {
|
||||
new_ignores.push((ancestor, None));
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten();
|
||||
let metadata = smol::block_on(fs.metadata(&ancestor.join(DOT_GIT)))
|
||||
.ok()
|
||||
.flatten();
|
||||
if metadata.is_some() {
|
||||
repo_root = Some(Arc::from(ancestor));
|
||||
break;
|
||||
@@ -2657,7 +2652,7 @@ impl BackgroundScannerState {
|
||||
.any(|p| entry.path.starts_with(p))
|
||||
}
|
||||
|
||||
async fn enqueue_scan_dir(
|
||||
fn enqueue_scan_dir(
|
||||
&self,
|
||||
abs_path: Arc<Path>,
|
||||
entry: &Entry,
|
||||
@@ -2665,10 +2660,7 @@ impl BackgroundScannerState {
|
||||
fs: &dyn Fs,
|
||||
) {
|
||||
let path = entry.path.clone();
|
||||
let ignore_stack = self
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&abs_path, true, fs)
|
||||
.await;
|
||||
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true, fs);
|
||||
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
||||
|
||||
if !ancestor_inodes.contains(&entry.inode) {
|
||||
@@ -2706,17 +2698,11 @@ impl BackgroundScannerState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_entry(
|
||||
&mut self,
|
||||
mut entry: Entry,
|
||||
fs: &dyn Fs,
|
||||
watcher: &dyn Watcher,
|
||||
) -> Entry {
|
||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry {
|
||||
self.reuse_entry_id(&mut entry);
|
||||
let entry = self.snapshot.insert_entry(entry, fs);
|
||||
if entry.path.file_name() == Some(&DOT_GIT) {
|
||||
self.insert_git_repository(entry.path.clone(), fs, watcher)
|
||||
.await;
|
||||
self.insert_git_repository(entry.path.clone(), fs, watcher);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2847,7 +2833,7 @@ impl BackgroundScannerState {
|
||||
self.snapshot.check_invariants(false);
|
||||
}
|
||||
|
||||
async fn insert_git_repository(
|
||||
fn insert_git_repository(
|
||||
&mut self,
|
||||
dot_git_path: Arc<RelPath>,
|
||||
fs: &dyn Fs,
|
||||
@@ -2888,11 +2874,10 @@ impl BackgroundScannerState {
|
||||
fs,
|
||||
watcher,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
async fn insert_git_repository_for_path(
|
||||
fn insert_git_repository_for_path(
|
||||
&mut self,
|
||||
work_directory: WorkDirectory,
|
||||
dot_git_abs_path: Arc<Path>,
|
||||
@@ -2914,7 +2899,7 @@ impl BackgroundScannerState {
|
||||
let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory);
|
||||
|
||||
let (repository_dir_abs_path, common_dir_abs_path) =
|
||||
discover_git_paths(&dot_git_abs_path, fs).await;
|
||||
discover_git_paths(&dot_git_abs_path, fs);
|
||||
watcher
|
||||
.add(&common_dir_abs_path)
|
||||
.context("failed to add common directory to watcher")
|
||||
@@ -3558,7 +3543,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
|
||||
}
|
||||
|
||||
struct BackgroundScanner {
|
||||
state: async_lock::Mutex<BackgroundScannerState>,
|
||||
state: Mutex<BackgroundScannerState>,
|
||||
fs: Arc<dyn Fs>,
|
||||
fs_case_sensitive: bool,
|
||||
status_updates_tx: UnboundedSender<ScanState>,
|
||||
@@ -3584,39 +3569,31 @@ impl BackgroundScanner {
|
||||
// If the worktree root does not contain a git repository, then find
|
||||
// the git repository in an ancestor directory. Find any gitignore files
|
||||
// in ancestor directories.
|
||||
let root_abs_path = self.state.lock().await.snapshot.abs_path.clone();
|
||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||
let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.extend(ignores);
|
||||
let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo {
|
||||
maybe!(async {
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.insert_git_repository_for_path(
|
||||
work_directory,
|
||||
ancestor_dot_git.clone().into(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)
|
||||
.await
|
||||
.log_err()?;
|
||||
Some(ancestor_dot_git)
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let containing_git_repository = repo.and_then(|(ancestor_dot_git, work_directory)| {
|
||||
self.state
|
||||
.lock()
|
||||
.insert_git_repository_for_path(
|
||||
work_directory,
|
||||
ancestor_dot_git.clone().into(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)
|
||||
.log_err()?;
|
||||
Some(ancestor_dot_git)
|
||||
});
|
||||
|
||||
log::trace!("containing git repository: {containing_git_repository:?}");
|
||||
|
||||
let mut global_gitignore_events =
|
||||
if let Some(global_gitignore_path) = &paths::global_gitignore_path() {
|
||||
self.state.lock().await.snapshot.global_gitignore =
|
||||
self.state.lock().snapshot.global_gitignore =
|
||||
if self.fs.is_file(&global_gitignore_path).await {
|
||||
build_gitignore(global_gitignore_path, self.fs.as_ref())
|
||||
.await
|
||||
@@ -3630,34 +3607,31 @@ impl BackgroundScanner {
|
||||
.await
|
||||
.0
|
||||
} else {
|
||||
self.state.lock().await.snapshot.global_gitignore = None;
|
||||
self.state.lock().snapshot.global_gitignore = None;
|
||||
Box::pin(futures::stream::empty())
|
||||
};
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.scan_id += 1;
|
||||
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
|
||||
let ignore_stack = state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref())
|
||||
.await;
|
||||
let ignore_stack = state.snapshot.ignore_stack_for_abs_path(
|
||||
root_abs_path.as_path(),
|
||||
true,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
|
||||
root_entry.is_ignored = true;
|
||||
state
|
||||
.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref())
|
||||
.await;
|
||||
state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
||||
}
|
||||
if root_entry.is_dir() {
|
||||
state
|
||||
.enqueue_scan_dir(
|
||||
root_abs_path.as_path().into(),
|
||||
&root_entry,
|
||||
&scan_job_tx,
|
||||
self.fs.as_ref(),
|
||||
)
|
||||
.await;
|
||||
state.enqueue_scan_dir(
|
||||
root_abs_path.as_path().into(),
|
||||
&root_entry,
|
||||
&scan_job_tx,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -3666,11 +3640,11 @@ impl BackgroundScanner {
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(true, scan_job_rx).await;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||
}
|
||||
|
||||
self.send_status_update(false, SmallVec::new()).await;
|
||||
self.send_status_update(false, SmallVec::new());
|
||||
|
||||
// Process any any FS events that occurred while performing the initial scan.
|
||||
// For these events, update events cannot be as precise, because we didn't
|
||||
@@ -3715,7 +3689,7 @@ impl BackgroundScanner {
|
||||
if did_scan {
|
||||
let abs_path =
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
state.path_prefixes_to_scan.insert(request.path.clone());
|
||||
state.snapshot.absolutize(&request.path)
|
||||
};
|
||||
@@ -3724,7 +3698,7 @@ impl BackgroundScanner {
|
||||
self.process_events(vec![abs_path]).await;
|
||||
}
|
||||
}
|
||||
self.send_status_update(false, request.done).await;
|
||||
self.send_status_update(false, request.done);
|
||||
}
|
||||
|
||||
paths = fs_events_rx.next().fuse() => {
|
||||
@@ -3753,7 +3727,7 @@ impl BackgroundScanner {
|
||||
request.relative_paths.sort_unstable();
|
||||
self.forcibly_load_paths(&request.relative_paths).await;
|
||||
|
||||
let root_path = self.state.lock().await.snapshot.abs_path.clone();
|
||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||
let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
|
||||
let root_canonical_path = match &root_canonical_path {
|
||||
Ok(path) => SanitizedPath::new(path),
|
||||
@@ -3775,7 +3749,7 @@ impl BackgroundScanner {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id;
|
||||
state.snapshot.scan_id += 1;
|
||||
if is_idle {
|
||||
@@ -3792,12 +3766,12 @@ impl BackgroundScanner {
|
||||
)
|
||||
.await;
|
||||
|
||||
self.send_status_update(scanning, request.done).await
|
||||
self.send_status_update(scanning, request.done)
|
||||
}
|
||||
|
||||
async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
|
||||
log::trace!("process events: {abs_paths:?}");
|
||||
let root_path = self.state.lock().await.snapshot.abs_path.clone();
|
||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||
let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
|
||||
let root_canonical_path = match &root_canonical_path {
|
||||
Ok(path) => SanitizedPath::new(path),
|
||||
@@ -3805,7 +3779,6 @@ impl BackgroundScanner {
|
||||
let new_path = self
|
||||
.state
|
||||
.lock()
|
||||
.await
|
||||
.snapshot
|
||||
.root_file_handle
|
||||
.clone()
|
||||
@@ -3838,31 +3811,24 @@ impl BackgroundScanner {
|
||||
let mut dot_git_abs_paths = Vec::new();
|
||||
abs_paths.sort_unstable();
|
||||
abs_paths.dedup_by(|a, b| a.starts_with(b));
|
||||
{
|
||||
let snapshot = &self.state.lock().await.snapshot;
|
||||
abs_paths.retain(|abs_path| {
|
||||
abs_paths.retain(|abs_path| {
|
||||
let abs_path = &SanitizedPath::new(abs_path);
|
||||
|
||||
|
||||
let snapshot = &self.state.lock().snapshot;
|
||||
{
|
||||
let mut is_git_related = false;
|
||||
|
||||
let dot_git_paths = self.executor.block(maybe!(async {
|
||||
let mut path = None;
|
||||
for ancestor in abs_path.as_path().ancestors() {
|
||||
|
||||
if is_git_dir(ancestor, self.fs.as_ref()).await {
|
||||
let dot_git_paths = abs_path.as_path().ancestors().find_map(|ancestor| {
|
||||
if smol::block_on(is_git_dir(ancestor, self.fs.as_ref())) {
|
||||
let path_in_git_dir = abs_path
|
||||
.as_path()
|
||||
.strip_prefix(ancestor)
|
||||
.expect("stripping off the ancestor");
|
||||
path = Some((ancestor.to_owned(), path_in_git_dir.to_owned()));
|
||||
break;
|
||||
Some((ancestor.to_owned(), path_in_git_dir.to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
path
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths {
|
||||
if skipped_files_in_dot_git
|
||||
@@ -3935,12 +3901,12 @@ impl BackgroundScanner {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if relative_paths.is_empty() && dot_git_abs_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.state.lock().await.snapshot.scan_id += 1;
|
||||
self.state.lock().snapshot.scan_id += 1;
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
log::debug!("received fs events {:?}", relative_paths);
|
||||
@@ -3954,29 +3920,29 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
|
||||
let affected_repo_roots = if !dot_git_abs_paths.is_empty() {
|
||||
self.update_git_repositories(dot_git_abs_paths).await
|
||||
self.update_git_repositories(dot_git_abs_paths)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
{
|
||||
let mut ignores_to_update = self.ignores_needing_update().await;
|
||||
let mut ignores_to_update = self.ignores_needing_update();
|
||||
ignores_to_update.extend(affected_repo_roots);
|
||||
let ignores_to_update = self.order_ignores(ignores_to_update).await;
|
||||
let snapshot = self.state.lock().await.snapshot.clone();
|
||||
let ignores_to_update = self.order_ignores(ignores_to_update);
|
||||
let snapshot = self.state.lock().snapshot.clone();
|
||||
self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update)
|
||||
.await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||
for (_, entry) in mem::take(&mut state.removed_entries) {
|
||||
state.scanned_dirs.remove(&entry.id);
|
||||
}
|
||||
}
|
||||
self.send_status_update(false, SmallVec::new()).await;
|
||||
self.send_status_update(false, SmallVec::new());
|
||||
}
|
||||
|
||||
async fn update_global_gitignore(&self, abs_path: &Path) {
|
||||
@@ -3985,30 +3951,30 @@ impl BackgroundScanner {
|
||||
.log_err()
|
||||
.map(Arc::new);
|
||||
let (prev_snapshot, ignore_stack, abs_path) = {
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.global_gitignore = ignore;
|
||||
let abs_path = state.snapshot.abs_path().clone();
|
||||
let ignore_stack = state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref())
|
||||
.await;
|
||||
let ignore_stack =
|
||||
state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref());
|
||||
(state.snapshot.clone(), ignore_stack, abs_path)
|
||||
};
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses_for_paths(
|
||||
scan_job_tx,
|
||||
prev_snapshot,
|
||||
vec![(abs_path, ignore_stack)],
|
||||
vec![(abs_path, ignore_stack)].into_iter(),
|
||||
)
|
||||
.await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
self.send_status_update(false, SmallVec::new()).await;
|
||||
self.send_status_update(false, SmallVec::new());
|
||||
}
|
||||
|
||||
async fn forcibly_load_paths(&self, paths: &[Arc<RelPath>]) -> bool {
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
let root_path = state.snapshot.abs_path.clone();
|
||||
for path in paths {
|
||||
for ancestor in path.ancestors() {
|
||||
@@ -4016,14 +3982,12 @@ impl BackgroundScanner {
|
||||
&& entry.kind == EntryKind::UnloadedDir
|
||||
{
|
||||
let abs_path = root_path.join(ancestor.as_std_path());
|
||||
state
|
||||
.enqueue_scan_dir(
|
||||
abs_path.into(),
|
||||
entry,
|
||||
&scan_job_tx,
|
||||
self.fs.as_ref(),
|
||||
)
|
||||
.await;
|
||||
state.enqueue_scan_dir(
|
||||
abs_path.into(),
|
||||
entry,
|
||||
&scan_job_tx,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
state.paths_to_scan.insert(path.clone());
|
||||
break;
|
||||
}
|
||||
@@ -4035,7 +3999,7 @@ impl BackgroundScanner {
|
||||
self.scan_dir(&job).await.log_err();
|
||||
}
|
||||
|
||||
!mem::take(&mut self.state.lock().await.paths_to_scan).is_empty()
|
||||
!mem::take(&mut self.state.lock().paths_to_scan).is_empty()
|
||||
}
|
||||
|
||||
async fn scan_dirs(
|
||||
@@ -4083,7 +4047,7 @@ impl BackgroundScanner {
|
||||
) {
|
||||
Ok(_) => {
|
||||
last_progress_update_count += 1;
|
||||
self.send_status_update(true, SmallVec::new()).await;
|
||||
self.send_status_update(true, SmallVec::new());
|
||||
}
|
||||
Err(count) => {
|
||||
last_progress_update_count = count;
|
||||
@@ -4108,12 +4072,8 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn send_status_update(
|
||||
&self,
|
||||
scanning: bool,
|
||||
barrier: SmallVec<[barrier::Sender; 1]>,
|
||||
) -> bool {
|
||||
let mut state = self.state.lock().await;
|
||||
fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
if state.changed_paths.is_empty() && scanning {
|
||||
return true;
|
||||
}
|
||||
@@ -4142,7 +4102,7 @@ impl BackgroundScanner {
|
||||
let root_abs_path;
|
||||
let root_char_bag;
|
||||
{
|
||||
let snapshot = &self.state.lock().await.snapshot;
|
||||
let snapshot = &self.state.lock().snapshot;
|
||||
if self.settings.is_path_excluded(&job.path) {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
@@ -4195,14 +4155,12 @@ impl BackgroundScanner {
|
||||
};
|
||||
|
||||
if child_name == DOT_GIT {
|
||||
let mut state = self.state.lock().await;
|
||||
state
|
||||
.insert_git_repository(
|
||||
child_path.clone(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let mut state = self.state.lock();
|
||||
state.insert_git_repository(
|
||||
child_path.clone(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
} else if child_name == GITIGNORE {
|
||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||
Ok(ignore) => {
|
||||
@@ -4222,7 +4180,7 @@ impl BackgroundScanner {
|
||||
|
||||
if self.settings.is_path_excluded(&child_path) {
|
||||
log::debug!("skipping excluded child entry {child_path:?}");
|
||||
self.state.lock().await.remove_path(&child_path);
|
||||
self.state.lock().remove_path(&child_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4322,7 +4280,7 @@ impl BackgroundScanner {
|
||||
new_entries.push(child_entry);
|
||||
}
|
||||
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
|
||||
// Identify any subdirectories that should not be scanned.
|
||||
let mut job_ix = 0;
|
||||
@@ -4404,7 +4362,7 @@ impl BackgroundScanner {
|
||||
None
|
||||
};
|
||||
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
let doing_recursive_update = scan_queue_tx.is_some();
|
||||
|
||||
// Remove any entries for paths that no longer exist or are being recursively
|
||||
@@ -4420,10 +4378,11 @@ impl BackgroundScanner {
|
||||
let abs_path: Arc<Path> = root_abs_path.join(path.as_std_path()).into();
|
||||
match metadata {
|
||||
Ok(Some((metadata, canonical_path))) => {
|
||||
let ignore_stack = state
|
||||
.snapshot
|
||||
.ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref())
|
||||
.await;
|
||||
let ignore_stack = state.snapshot.ignore_stack_for_abs_path(
|
||||
&abs_path,
|
||||
metadata.is_dir,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
let is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||
let mut fs_entry = Entry::new(
|
||||
path.clone(),
|
||||
@@ -4455,22 +4414,18 @@ impl BackgroundScanner {
|
||||
|| (fs_entry.path.is_empty()
|
||||
&& abs_path.file_name() == Some(OsStr::new(DOT_GIT)))
|
||||
{
|
||||
state
|
||||
.enqueue_scan_dir(
|
||||
abs_path,
|
||||
&fs_entry,
|
||||
scan_queue_tx,
|
||||
self.fs.as_ref(),
|
||||
)
|
||||
.await;
|
||||
state.enqueue_scan_dir(
|
||||
abs_path,
|
||||
&fs_entry,
|
||||
scan_queue_tx,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
} else {
|
||||
fs_entry.kind = EntryKind::UnloadedDir;
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref())
|
||||
.await;
|
||||
state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
||||
|
||||
if path.is_empty()
|
||||
&& let Some((ignores, repo)) = new_ancestor_repo.take()
|
||||
@@ -4485,7 +4440,6 @@ impl BackgroundScanner {
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
@@ -4524,11 +4478,11 @@ impl BackgroundScanner {
|
||||
&self,
|
||||
scan_job_tx: Sender<ScanJob>,
|
||||
prev_snapshot: LocalSnapshot,
|
||||
ignores_to_update: Vec<(Arc<Path>, IgnoreStack)>,
|
||||
mut ignores_to_update: impl Iterator<Item = (Arc<Path>, IgnoreStack)>,
|
||||
) {
|
||||
let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
|
||||
{
|
||||
for (parent_abs_path, ignore_stack) in ignores_to_update {
|
||||
while let Some((parent_abs_path, ignore_stack)) = ignores_to_update.next() {
|
||||
ignore_queue_tx
|
||||
.send_blocking(UpdateIgnoreStatusJob {
|
||||
abs_path: parent_abs_path,
|
||||
@@ -4569,11 +4523,11 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
|
||||
fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
|
||||
let mut ignores_to_update = Vec::new();
|
||||
|
||||
{
|
||||
let snapshot = &mut self.state.lock().await.snapshot;
|
||||
let snapshot = &mut self.state.lock().snapshot;
|
||||
let abs_path = snapshot.abs_path.clone();
|
||||
snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
@@ -4601,27 +4555,26 @@ impl BackgroundScanner {
|
||||
ignores_to_update
|
||||
}
|
||||
|
||||
async fn order_ignores(&self, mut ignores: Vec<Arc<Path>>) -> Vec<(Arc<Path>, IgnoreStack)> {
|
||||
fn order_ignores(
|
||||
&self,
|
||||
mut ignores: Vec<Arc<Path>>,
|
||||
) -> impl use<> + Iterator<Item = (Arc<Path>, IgnoreStack)> {
|
||||
let fs = self.fs.clone();
|
||||
let snapshot = self.state.lock().await.snapshot.clone();
|
||||
let snapshot = self.state.lock().snapshot.clone();
|
||||
ignores.sort_unstable();
|
||||
let mut ignores_to_update = ignores.into_iter().peekable();
|
||||
|
||||
let mut result = vec![];
|
||||
while let Some(parent_abs_path) = ignores_to_update.next() {
|
||||
std::iter::from_fn(move || {
|
||||
let parent_abs_path = ignores_to_update.next()?;
|
||||
while ignores_to_update
|
||||
.peek()
|
||||
.map_or(false, |p| p.starts_with(&parent_abs_path))
|
||||
{
|
||||
ignores_to_update.next().unwrap();
|
||||
}
|
||||
let ignore_stack = snapshot
|
||||
.ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref())
|
||||
.await;
|
||||
result.push((parent_abs_path, ignore_stack));
|
||||
}
|
||||
|
||||
result
|
||||
let ignore_stack =
|
||||
snapshot.ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref());
|
||||
Some((parent_abs_path, ignore_stack))
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||
@@ -4653,7 +4606,7 @@ impl BackgroundScanner {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await
|
||||
if let Ok(Some(metadata)) = smol::block_on(self.fs.metadata(&job.abs_path.join(DOT_GIT)))
|
||||
&& metadata.is_dir
|
||||
{
|
||||
ignore_stack.repo_root = Some(job.abs_path.clone());
|
||||
@@ -4673,16 +4626,14 @@ impl BackgroundScanner {
|
||||
|
||||
// Scan any directories that were previously ignored and weren't previously scanned.
|
||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||
let state = self.state.lock().await;
|
||||
let state = self.state.lock();
|
||||
if state.should_scan_directory(&entry) {
|
||||
state
|
||||
.enqueue_scan_dir(
|
||||
abs_path.clone(),
|
||||
&entry,
|
||||
&job.scan_queue,
|
||||
self.fs.as_ref(),
|
||||
)
|
||||
.await;
|
||||
state.enqueue_scan_dir(
|
||||
abs_path.clone(),
|
||||
&entry,
|
||||
&job.scan_queue,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4706,7 +4657,7 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
let state = &mut self.state.lock().await;
|
||||
let state = &mut self.state.lock();
|
||||
for edit in &entries_by_path_edits {
|
||||
if let Edit::Insert(entry) = edit
|
||||
&& let Err(ix) = state.changed_paths.binary_search(&entry.path)
|
||||
@@ -4722,9 +4673,9 @@ impl BackgroundScanner {
|
||||
state.snapshot.entries_by_id.edit(entries_by_id_edits, ());
|
||||
}
|
||||
|
||||
async fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Vec<Arc<Path>> {
|
||||
fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Vec<Arc<Path>> {
|
||||
log::trace!("reloading repositories: {dot_git_paths:?}");
|
||||
let mut state = self.state.lock().await;
|
||||
let mut state = self.state.lock();
|
||||
let scan_id = state.snapshot.scan_id;
|
||||
let mut affected_repo_roots = Vec::new();
|
||||
for dot_git_dir in dot_git_paths {
|
||||
@@ -4754,15 +4705,13 @@ impl BackgroundScanner {
|
||||
return Vec::new();
|
||||
};
|
||||
affected_repo_roots.push(dot_git_dir.parent().unwrap().into());
|
||||
state
|
||||
.insert_git_repository(
|
||||
RelPath::new(relative, PathStyle::local())
|
||||
.unwrap()
|
||||
.into_arc(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)
|
||||
.await;
|
||||
state.insert_git_repository(
|
||||
RelPath::new(relative, PathStyle::local())
|
||||
.unwrap()
|
||||
.into_arc(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
}
|
||||
Some(local_repository) => {
|
||||
state.snapshot.git_repositories.update(
|
||||
@@ -4790,7 +4739,7 @@ impl BackgroundScanner {
|
||||
|
||||
if exists_in_snapshot
|
||||
|| matches!(
|
||||
self.fs.metadata(&entry.common_dir_abs_path).await,
|
||||
smol::block_on(self.fs.metadata(&entry.common_dir_abs_path)),
|
||||
Ok(Some(_))
|
||||
)
|
||||
{
|
||||
@@ -5549,13 +5498,11 @@ fn parse_gitfile(content: &str) -> anyhow::Result<&Path> {
|
||||
Ok(Path::new(path.trim()))
|
||||
}
|
||||
|
||||
async fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, Arc<Path>) {
|
||||
fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, Arc<Path>) {
|
||||
let mut repository_dir_abs_path = dot_git_abs_path.clone();
|
||||
let mut common_dir_abs_path = dot_git_abs_path.clone();
|
||||
|
||||
if let Some(path) = fs
|
||||
.load(dot_git_abs_path)
|
||||
.await
|
||||
if let Some(path) = smol::block_on(fs.load(dot_git_abs_path))
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|contents| parse_gitfile(contents).log_err())
|
||||
@@ -5564,19 +5511,17 @@ async fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<P
|
||||
.parent()
|
||||
.unwrap_or(Path::new(""))
|
||||
.join(path);
|
||||
if let Some(path) = fs.canonicalize(&path).await.log_err() {
|
||||
if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() {
|
||||
repository_dir_abs_path = Path::new(&path).into();
|
||||
common_dir_abs_path = repository_dir_abs_path.clone();
|
||||
|
||||
if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok()
|
||||
&& let Some(commondir_path) = fs
|
||||
.canonicalize(&path.join(commondir_contents.trim()))
|
||||
.await
|
||||
.log_err()
|
||||
if let Some(commondir_contents) = smol::block_on(fs.load(&path.join("commondir"))).ok()
|
||||
&& let Some(commondir_path) =
|
||||
smol::block_on(fs.canonicalize(&path.join(commondir_contents.trim()))).log_err()
|
||||
{
|
||||
common_dir_abs_path = commondir_path.as_path().into();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(repository_dir_abs_path, common_dir_abs_path)
|
||||
}
|
||||
|
||||
@@ -734,6 +734,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
worktree.read_with(cx, |tree, _| {
|
||||
let tracked = tree
|
||||
.entry_for_path(rel_path("tracked-dir/file.txt"))
|
||||
@@ -1536,7 +1537,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
||||
assert_eq!(
|
||||
updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
|
||||
final_snapshot.entries(true, 0).collect::<Vec<_>>(),
|
||||
"wrong updates after snapshot {i}: {updates:#?}",
|
||||
"wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fs.workspace = true
|
||||
gpui = { workspace = true, features = ["windows-manifest"] }
|
||||
settings.workspace = true
|
||||
worktree.workspace = true
|
||||
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,54 +0,0 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
|
||||
use fs::RealFs;
|
||||
use gpui::Application;
|
||||
use settings::Settings;
|
||||
use worktree::{Worktree, WorktreeSettings};
|
||||
|
||||
fn main() {
|
||||
let Some(worktree_root_path) = std::env::args().nth(1) else {
|
||||
println!(
|
||||
"Missing path to worktree root\nUsage: bench_background_scan PATH_TO_WORKTREE_ROOT"
|
||||
);
|
||||
return;
|
||||
};
|
||||
let app = Application::headless();
|
||||
|
||||
app.run(|cx| {
|
||||
settings::init(cx);
|
||||
WorktreeSettings::register(cx);
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let worktree = Worktree::local(
|
||||
Path::new(&worktree_root_path),
|
||||
true,
|
||||
fs,
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.expect("Worktree initialization to succeed");
|
||||
let did_finish_scan = worktree
|
||||
.update(cx, |this, _| this.as_local().unwrap().scan_complete())
|
||||
.unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
did_finish_scan.await;
|
||||
let elapsed = start.elapsed();
|
||||
let (files, directories) = worktree
|
||||
.read_with(cx, |this, _| (this.file_count(), this.dir_count()))
|
||||
.unwrap();
|
||||
println!(
|
||||
"{:?} for {directories} directories and {files} files",
|
||||
elapsed
|
||||
);
|
||||
cx.update(|cx| {
|
||||
cx.quit();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
@@ -185,18 +185,8 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
|
||||
editor::actions::SelectPreviousSyntaxNode,
|
||||
),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Add Cursor Above",
|
||||
editor::actions::AddSelectionAbove {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
),
|
||||
MenuItem::action(
|
||||
"Add Cursor Below",
|
||||
editor::actions::AddSelectionBelow {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
),
|
||||
MenuItem::action("Add Cursor Above", editor::actions::AddSelectionAbove),
|
||||
MenuItem::action("Add Cursor Below", editor::actions::AddSelectionBelow),
|
||||
MenuItem::action(
|
||||
"Select Next Occurrence",
|
||||
editor::actions::SelectNext {
|
||||
|
||||
@@ -266,18 +266,8 @@ impl Render for QuickActionBar {
|
||||
)
|
||||
.action("Expand Selection", Box::new(SelectLargerSyntaxNode))
|
||||
.action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
|
||||
.action(
|
||||
"Add Cursor Above",
|
||||
Box::new(AddSelectionAbove {
|
||||
skip_soft_wrap: true,
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
"Add Cursor Below",
|
||||
Box::new(AddSelectionBelow {
|
||||
skip_soft_wrap: true,
|
||||
}),
|
||||
)
|
||||
.action("Add Cursor Above", Box::new(AddSelectionAbove))
|
||||
.action("Add Cursor Below", Box::new(AddSelectionBelow))
|
||||
.separator()
|
||||
.action("Go to Symbol", Box::new(ToggleOutline))
|
||||
.action("Go to Line/Column", Box::new(ToggleGoToLine))
|
||||
|
||||
@@ -63,7 +63,7 @@ For more information, see the [Gemini CLI docs](https://github.com/google-gemini
|
||||
Similar to Zed's first-party agent, you can use Gemini CLI to do anything that you need.
|
||||
And to give it context, you can @-mention files, recent threads, symbols, or fetch the web.
|
||||
|
||||
> Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, and checkpointing.
|
||||
> Note that some first-party agent features don't yet work with Gemini CLI: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects.
|
||||
> We hope to add these features in the near future.
|
||||
|
||||
## Claude Code
|
||||
@@ -113,27 +113,6 @@ If you want to override the executable used by the adapter, you can set the `CLA
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Similar to Zed's first-party agent, you can use Claude Code to do anything that you need.
|
||||
And to give it context, you can @-mention files, recent threads, symbols, or fetch the web.
|
||||
|
||||
In complement to talking to it [over ACP](https://agentclientprotocol.com), Zed relies on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview) to support some of its specific features.
|
||||
However, the SDK doesn't yet expose everything needed to fully support all of them:
|
||||
|
||||
- Slash Commands: A subset of [built-in commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#built-in-slash-commands) are supported, while [custom slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#custom-slash-commands) are fully supported.
|
||||
- [Subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents) are supported.
|
||||
- [Hooks](https://docs.anthropic.com/en/docs/claude-code/hooks-guide) are currently _not_ supported.
|
||||
|
||||
> Also note that some [first-party agent](./agent-panel.md) features don't yet work with Claude Code: editing past messages, resuming threads from history, and checkpointing.
|
||||
> We hope to add these features in the near future.
|
||||
|
||||
#### CLAUDE.md
|
||||
|
||||
Claude Code in Zed will automatically use any `CLAUDE.md` file found in your project root, project subdirectories, or root `.claude` directory.
|
||||
|
||||
If you don't have a `CLAUDE.md` file, you can ask Claude Code to create one for you through the `init` slash command.
|
||||
|
||||
## Codex CLI
|
||||
|
||||
You can also run [Codex CLI](https://github.com/openai/codex) directly via Zed's [agent panel](./agent-panel.md).
|
||||
@@ -175,12 +154,25 @@ Zed will always use this managed version of Codex even if you have it installed
|
||||
|
||||
### Usage
|
||||
|
||||
Similar to Zed's first-party agent, you can use Codex to do anything that you need.
|
||||
And to give it context, you can @-mention files, symbols, or fetch the web.
|
||||
Similar to Zed's first-party agent, you can use Claude Code to do anything that you need.
|
||||
And to give it context, you can @-mention files, recent threads, symbols, or fetch the web.
|
||||
|
||||
> Note that some first-party agent features don't yet work with Codex: editing past messages, resuming threads from history, and checkpointing.
|
||||
In complement to talking to it [over ACP](https://agentclientprotocol.com), Zed relies on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview) to support some of its specific features.
|
||||
However, the SDK doesn't yet expose everything needed to fully support all of them:
|
||||
|
||||
- Slash Commands: A subset of [built-in commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#built-in-slash-commands) are supported, while [custom slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands#custom-slash-commands) are fully supported.
|
||||
- [Subagents](https://docs.anthropic.com/en/docs/claude-code/sub-agents) are supported.
|
||||
- [Hooks](https://docs.anthropic.com/en/docs/claude-code/hooks-guide) are currently _not_ supported.
|
||||
|
||||
> Also note that some [first-party agent](./agent-panel.md) features don't yet work with Claude Code: editing past messages, resuming threads from history, checkpointing, and using the agent in SSH projects.
|
||||
> We hope to add these features in the near future.
|
||||
|
||||
#### CLAUDE.md
|
||||
|
||||
Claude Code in Zed will automatically use any `CLAUDE.md` file found in your project root, project subdirectories, or root `.claude` directory.
|
||||
|
||||
If you don't have a `CLAUDE.md` file, you can ask Claude Code to create one for you through the `init` slash command.
|
||||
|
||||
## Add Custom Agents {#add-custom-agents}
|
||||
|
||||
You can run any agent speaking ACP in Zed by changing your settings as follows:
|
||||
|
||||
@@ -320,18 +320,13 @@ To run linter fixes automatically on save:
|
||||
```json [settings]
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll.eslint"
|
||||
},
|
||||
"auto"
|
||||
]
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This specifies that when a format is requested, Zed will first run the `source.fixAll.eslint` action, and then perform the actual format using the language-specific default formatter (for JavaScript this is Prettier, and for other languages it often corresponds to formatting using the default language server).
|
||||
|
||||
### Integrating Formatting and Linting
|
||||
|
||||
Zed allows you to run both formatting and linting on save. Here's an example that uses Prettier for formatting and ESLint for linting JavaScript files:
|
||||
|
||||
@@ -4155,8 +4155,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
|
||||
},
|
||||
"hide_root": false,
|
||||
"hide_hidden": false,
|
||||
"starts_open": true,
|
||||
"open_file_on_paste": true
|
||||
"starts_open": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -49,12 +49,9 @@ You can configure Zed to format code using `eslint --fix` by running the ESLint
|
||||
{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll.eslint"
|
||||
},
|
||||
"auto"
|
||||
]
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,12 +63,9 @@ You can also only execute a single ESLint rule when using `fixAll`:
|
||||
{
|
||||
"languages": {
|
||||
"JavaScript": {
|
||||
"formatter": [
|
||||
{
|
||||
"code_action": "source.fixAll.eslint"
|
||||
},
|
||||
"auto"
|
||||
]
|
||||
"formatter": {
|
||||
"code_action": "source.fixAll.eslint"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lsp": {
|
||||
|
||||
Reference in New Issue
Block a user