Compare commits

..

1 Commits

Author SHA1 Message Date
Ben Kunkle
bd676ffc4d Update title_bar.rs 2025-10-15 15:35:03 -05:00
83 changed files with 1108 additions and 2171 deletions

48
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
}
},
{

View File

@@ -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

View File

@@ -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)

View File

@@ -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()),
)
})

View File

@@ -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",

View File

@@ -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(|_| {

View File

@@ -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)
}
}

View File

@@ -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);
}

View File

@@ -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",
}
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)]

View File

@@ -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());
}
}
}

View File

@@ -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,
})
}

View File

@@ -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.

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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:?}")),
});

View File

@@ -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

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -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();
});
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -328,6 +328,7 @@ impl Render for PaintingViewer {
let dashed = self.dashed;
div()
.font_family(".SystemUIFont")
.bg(gpui::white())
.size_full()
.p_4()

View File

@@ -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,

View File

@@ -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)?;
};

View File

@@ -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();

View File

@@ -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(),

View File

@@ -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")
],
}
}

View File

@@ -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 {

View File

@@ -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,
);

View File

@@ -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

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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<()> {

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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(())
}

View File

@@ -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),
})
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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());
},
)?;
}

View File

@@ -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(),

View File

@@ -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(),
}
}

View File

@@ -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)]

View File

@@ -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(

View File

@@ -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);
}
}

View File

@@ -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()
);
}

View File

@@ -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",

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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

View File

@@ -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()
),
])

View File

@@ -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)
}

View File

@@ -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};

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -24,7 +24,6 @@ test-support = [
[dependencies]
anyhow.workspace = true
async-lock.workspace = true
clock.workspace = true
collections.workspace = true
fs.workspace = true

View File

@@ -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)
}

View File

@@ -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:#?}",
);
}
}

View File

@@ -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

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -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();
})
}

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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:

View File

@@ -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:

View File

@@ -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
}
}
```

View File

@@ -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": {