Compare commits

...

47 Commits

Author SHA1 Message Date
Zed Bot
8f1d050bfa Bump to 0.209.7 for @osiewicz 2025-10-27 15:13:34 +00:00
Piotr Osiewicz
d54e024f94 lsp: Support tracking multiple registrations of diagnostic providers (#41096)
Closes #40966
Closes #41195
Closes #40980

Release Notes:

- Fixed diagnostics not working with basedpyright/pyright beyond an
initial version of the document

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-10-27 15:53:15 +01:00
Joseph T. Lyons
3d61fb08a0 zed 0.209.6 2025-10-24 13:31:37 -04:00
Ben Kunkle
419f9a865c Don't migrate empty formatter array (#40932)
Follow up for #40409
Fix for
https://github.com/zed-industries/zed/issues/40874#issuecomment-3433759849

Release Notes:

- Fixed an issue where having an empty formatter array in your settings
`"formatter": []` would result in an erroneous prompt to migrate
settings
2025-10-22 16:09:51 -04:00
Zed Bot
68bb196071 Bump to 0.209.5 for @ConradIrwin 2025-10-22 18:10:07 +00:00
Lukas Wirth
12acc1db10 acp_thread: Fix panic when following acp agents across buffers (#40798)
Fixes ZED-2D7

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-22 12:02:50 -06:00
Joseph T. Lyons
34a35b8443 v0.209.x stable 2025-10-22 10:15:36 -04:00
Owen Law
bdd0f2fb0a Disable slang-server for verilog extension (#40442)
Should be merged with
https://github.com/zed-industries/extensions/pull/3584, which adds
`slang-server` as a new language server, but should be disabled by
default due to an issue with it not initializing on Windows and being a
relatively new language server in general.

Release Notes:

- N/A
2025-10-22 11:55:16 +02:00
Smit Barmase
4c8d47e425 Add Vue language server v3 support (#40651)
Closes https://github.com/zed-extensions/vue/issues/48

Migration guide:
https://github.com/vuejs/language-tools/discussions/5456

PR to remove tdsk: https://github.com/zed-extensions/vue/pull/61

Release Notes:

- Added support for Vue language server version 3. Know more
[here](https://github.com/vuejs/language-tools/releases/tag/v3.0.0).

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-10-22 11:34:29 +05:30
Mikayla Maki
35b2a6797f zed 0.209.4 2025-10-21 19:08:51 -07:00
Max Brunsfeld
4cf4794ac7 Fix extraction of font runs from text runs (#40840)
Fixes a bug in https://github.com/zed-industries/zed/pull/39928

The bug caused all completions to appear in bold-face

Release Notes:

- Fixed a bug where bold-face font was applied to the wrong characters
in items in the autocomplete menu

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-10-21 19:07:12 -07:00
Marshall Bowers
d2b0c6002b Switch to fork of async-tar (#40828)
This PR switches to our own fork of `async-tar`.

Release Notes:

- N/A
2025-10-21 18:06:02 -04:00
Cole Miller
898975660a Fix crash when opening files with a BOM on macOS (#40419)
Closes #40359

We were segfaulting when opening a UTF-8 file starting with a byte order
mark due to a mismatch in our UTF-16 indexing calculations caused by
Core Foundations `replace_str` stripping the BOM internally. This PR
fixes the crash by replacing one of our manual calculations by calling
the Core Foundations API to get the length of a string.

Release Notes:

- Fixed a crash on macOS when opening a file that starts with a UTF-8
byte order mark (BOM).

Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
2025-10-21 11:10:39 -07:00
Zed Bot
be4bfa3bfe Bump to 0.209.3 for @ConradIrwin 2025-10-21 17:54:58 +00:00
Lukas Wirth
55ffc41b1c file_finder: Fix open path prompt creating wrong highlight indices (#40488)
Fixes ZED-28R

Release Notes:

- Fixed open path prompt panicking on certain inputs
2025-10-21 17:57:47 +02:00
Dino
b631e813e4 vim: Fix hang in visual block motion (#40723)
The `vim::visual::Vim.visual_block_motion` method was recently updated
(https://github.com/zed-industries/zed/pull/39355) in order to jump
between buffer rows instead of display rows. However, with this now
being the case, the `break` condition was never met when the motion was
horizontal rather than vertical and soft wrapped lines were used. As
such, this commit udpates the condition to ensure it's always reached,
preventing the hanging from happening.

Release Notes:

- Fixed hang in Vim's visual block motions when updating selections

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-10-21 12:43:24 +01:00
Finn Evers
5e64237b59 Make kotlin-lsp the default language server (#40776)
Following a conversation with the maintainer/owner of
kotlin-language-server, he recommended switching to the official
language server, which is better in many aspects and also more actively
maintained.

Release Notes:

- Made the official Kotlin Language Server the default language server
for Kotlin.
2025-10-21 11:04:59 +02:00
Ben Brandt
e82a6b5c3d acp: Fix following for agents that only provide locations (#40710)
We were dropping the entities once we created the buffers, so the weak
entities could never be upgraded. This treats new locations we see the
same as we would for a read/write call and stores the entity so that we
can follow like we normally would.

Release Notes:

- acp: Fix following not working with certain tool calls.
2025-10-21 10:45:47 +02:00
Finn Evers
8ae69036b8 editor: Toggle diff hunk based on current mouse position (#40773)
This fixes an issue where we would search for the hovered diff hunk
based on the mouse hit test computed during (or prior) editor paint
instead of the mouse hit test computed prior to the mouse event
invocation.

That in turn could lead to cases where moving the mouse from the editor
to the project panel and then clicking a file shortly after would expand
a diff hunk when actually nothing should happen in that case.

Release Notes:

- Fixed an issue where diff hunks would sometimes erroneously toggle
upon mouse clicks.
2025-10-21 10:42:13 +02:00
Cole Miller
562edf45ef Don't auto-release preview (#40728)
This feels a bit dangerous as long as we have the split releases problem

Release Notes:

- N/A
2025-10-20 21:29:36 -04:00
Kirill Bulatov
3eb1952a6c Fix inlay hint cleanup on excerpts removal (#40738)
A cherry-pick of
f5188d55fb

This fixes a hard-to-reproduce crash caused excerpts removal not
updating previous snapshot data after corresponding inlay data was
removed.
Same branch has a test:
8783a9eb4f
that does not fail on `main` due to different way inlays are queried, it
will be merged later.

Release Notes:

- N/A
2025-10-21 00:44:42 +03:00
Cole Miller
a808ecb503 zed 0.209.2 2025-10-20 12:21:33 -04:00
Danilo Leal
b69d0ab1be ai onboarding: Add dismiss button to the sign in banner (#40660)
Release Notes:

- N/A
2025-10-20 12:09:28 -04:00
Cole Miller
eb33d3009c Set the minimum log level to info for the remote server (#40543)
`env_logger` defaults to only showing error-level logs, but we show
info-level logs and above for the main Zed process, so I think it makes
sense for the remote server to behave the same way.

Release Notes:

- N/A
2025-10-20 10:36:46 -04:00
Conrad Irwin
caa5d624ea Disallow rename/copy/delete on unshared files (#40540)
Release Notes:

- Disallow rename/delete/copy on unshared files

Co-Authored-By: Cole <cole@zed.dev>
2025-10-17 16:34:43 -06:00
Cole Miller
de7e0b47ba windows: Unpin Gemini CLI (#40288)
Updates #40212

v0.9.0 is now stable and contains the fix for the line endings bug, so
we can return to installing from the stable channel as usual. This also
bumps the minimum version on Windows to v0.9.0 so that anyone on v0.8.x
or v0.9.0-preview.4 will be upgraded automatically.

Release Notes:

- N/A
2025-10-16 21:00:16 -04:00
Zed Bot
bebf4b0497 Bump to 0.209.1 for @probably-neb 2025-10-16 19:29:09 +00:00
Ben Kunkle
fe00b6cb53 Revert deprecate code actions on format (#40409)
Closes #40334

This reverts the change made in #39983, and includes a replacement
migration that will transform formatter settings values consisting of
only `code_action` format steps into the previously deprecated
`code_actions_on_format` in an attempt to restore the behavior to what
it was before the migration that deprecated `code_actions_on_format`.

This PR will result in a modified order in the `code_actions_on_format`
setting if it existed, however the decision was made to explicitly
ignore this for now, as this PR is primarily targeting users who have
already had the deprecation migration run, and no longer have the
`code_actions_on_format` key

Release Notes:

- Fixed an issue with a settings migration that deprecated the
`code_actions_on_format` setting. The `code_actions_on_format` setting
has been un-deprecated, and affected users will have the bad migration
rolled back with an updated migration

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
2025-10-16 14:30:30 -04:00
Lukas Wirth
70d197876e editor: Fix SelectionsCollection::disjoint not being ordered correctly (#40249)
We've been seeing the occasional `cannot seek backwards` panic within
`SelectionsCollection` without means to reproduce.

I believe the cause is one of the callers of
`MutableSelectionsCollection::select` not passing a well formed
`Selection` where `start > end`, so this PR enforces the invariant in
`select` by swapping the fields and setting `reversed` as required as
the other mutator functions already do that as well.

We could also just assert this instead, but it callers usually won't
care about this so its the less user facing annoyance to just fix this
invariant up internally.

Fixes ZED-253
Fixes ZED-ZJ
Fixes ZED-23S
Fixes ZED-222
Fixes ZED-1ZV
Fixes ZED-1SN
Fixes ZED-1Z0
Fixes ZED-10E
Fixes ZED-1X0
Fixes ZED-12M
Fixes ZED-1GR
Fixes ZED-1VE
Fixes ZED-13X
Fixes ZED-1G4

Release Notes:

- Fixed occasional panics when querying selections
2025-10-16 18:49:40 +02:00
Lukas Wirth
10c540bf20 windows: Fix panic when quitting dialogs that do not have a cancel button (#40348)
`TaskDialogIndirect` may return `IDCANCEL` when the user quits the
dialog via escape or alt+f4, so we need to account for that.

Fixes ZED-25H

Release Notes:

- Fixed panic when hitting escape in dialogs on windows
2025-10-16 18:49:10 +02:00
Lukas Wirth
bb7de4ee04 windows: Fix occasional RefCell already mutably borrowed panic (#40336)
Release Notes:

- Fixed occasional `RefCell already mutably borrowed` panic in windows
event handling
2025-10-16 18:48:57 +02:00
Lukas Wirth
87b9b9f452 languages: Fix go completion labels creating out of bounds highlight runs (#40355)
Fixes ZED-26Q

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-16 18:48:22 +02:00
Lukas Wirth
e17609c938 editor: Fix invalid excerpt panic in Editor::hover_links (#40387)
Fixes ZED-17N
Fixes ZED-26Z

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-16 18:48:05 +02:00
Julia Ryan
353e936a7a Fix mkdir nushell flags (#40306)
Closes #40269

Release Notes:

- N/A
2025-10-16 18:20:49 +02:00
Ben Brandt
1d89ddb776 Revert "acp: Don't collapse tool calls by default" (#40395)
Reverts zed-industries/zed#40164

Release Notes:

- N/A
2025-10-16 18:20:49 +02:00
Lukas Wirth
c8903a0010 markdown_preview: Fix alt text causing mismatched highlighting runs (#40374)
Fixes ZED-277

Release Notes:

- Fixed alt text in markdown preview creating inconsistent highlighting
2025-10-16 18:20:49 +02:00
Julia Ryan
ac002d0a7f Add winget release job (#40293)
This will automatically open PRs against the winget package registry to
bump our version there when we do a release.

Release Notes:

- N/A
2025-10-16 08:31:33 -07:00
Ben Brandt
66112d81b0 acp: Add nicer WSL warning for Codex (#40354)
Moves the Codex warning into the thread so that we can render it nicer,
as well as provide an option to open the folder in WSL.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-10-16 12:38:35 +02:00
Ben Kunkle
c3eaa757e8 settings_ui: Scale window size based on UI font size (#40257)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 21:16:02 -04:00
localcc
2793dd77ad Fix duplicate WSL entries (#40255)
Release Notes:

- N/A
2025-10-15 16:33:53 +02:00
localcc
10720d64a3 Improve musl libc detection (#40254)
Release Notes:

- N/A
2025-10-15 16:33:28 +02:00
Ben Brandt
2adb979ed7 acp: Fix /logout for agents that support it (#40248)
We were clearing the message editor too early. We only want to clear the
message editor if we are going to short circuit and return early before
submitting.
Otherwise, the agents that can handle this themselves won't have the
ability to do so.

Release Notes:

- acp: Fix /logout not working for some agents
2025-10-15 08:25:55 -06:00
Ben Brandt
bec6cd94a4 acp: Allow updating default mode for Codex (#40238)
Release Notes:

- acp: Save default mode for codex
2025-10-15 08:25:55 -06:00
Conrad Irwin
f6c0fa43ef Avoid gap between titlebar and body on linux
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-14 22:41:34 -06:00
Cole Miller
62da0dc402 Fix triggers for debugger thread and session lists not rendering (#40227)
Release Notes:

- N/A
2025-10-14 22:37:49 -06:00
Cole Miller
154405ff3b Fix a couple of bugs in remote browser debugging implementation (#40225)
Follow-up to #39248 

- Correctly forward ports over SSH, including the port from the debug
scenario's `url`
- Give the companion time to start up, instead of bailing if the first
connection attempt fails

Release Notes:

- Fixed not being able to launch a browser debugging session in an SSH
project.
2025-10-14 23:23:27 -04:00
Mikayla Maki
b558313181 v0.209.x preview 2025-10-14 17:33:03 -07:00
58 changed files with 1384 additions and 920 deletions

View File

@@ -882,7 +882,8 @@ jobs:
auto-release-preview:
name: Auto release preview
if: |
startsWith(github.ref, 'refs/tags/v')
false
&& startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
runs-on:

View File

@@ -38,6 +38,26 @@ jobs:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest

5
Cargo.lock generated
View File

@@ -1336,8 +1336,7 @@ dependencies = [
[[package]]
name = "async-tar"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8"
source = "git+https://github.com/zed-industries/async-tar?rev=8af312477196311c9ea4097f2a22022f6d609bf6#8af312477196311c9ea4097f2a22022f6d609bf6"
dependencies = [
"async-std",
"filetime",
@@ -21203,7 +21202,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.209.0"
version = "0.209.7"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -457,7 +457,7 @@ async-dispatcher = "0.1"
async-fs = "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"
async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" }
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"

View File

@@ -1527,6 +1527,7 @@
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
},
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
@@ -1696,7 +1697,9 @@
"preferred_line_length": 72
},
"Go": {
"formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
"code_actions_on_format": {
"source.organizeImports": true
},
"debuggers": ["Delve"]
},
"GraphQL": {
@@ -1735,7 +1738,7 @@
}
},
"Kotlin": {
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
},
"LaTeX": {
"formatter": "language_server",
@@ -1813,10 +1816,11 @@
},
"SystemVerilog": {
"format_on_save": "off",
"language_servers": ["!slang", "..."],
"use_on_type_format": false
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"language_servers": ["vue-language-server", "vtsls", "..."],
"prettier": {
"allowed": true
}

View File

@@ -328,7 +328,7 @@ impl ToolCall {
location: acp::ToolCallLocation,
project: WeakEntity<Project>,
cx: &mut AsyncApp,
) -> Option<AgentLocation> {
) -> Option<ResolvedLocation> {
let buffer = project
.update(cx, |project, cx| {
project
@@ -350,17 +350,14 @@ impl ToolCall {
})
.ok()?;
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
})
Some(ResolvedLocation { buffer, position })
}
fn resolve_locations(
&self,
project: Entity<Project>,
cx: &mut App,
) -> Task<Vec<Option<AgentLocation>>> {
) -> Task<Vec<Option<ResolvedLocation>>> {
let locations = self.locations.clone();
project.update(cx, |_, cx| {
cx.spawn(async move |project, cx| {
@@ -374,6 +371,23 @@ impl ToolCall {
}
}
// Separate so we can hold a strong reference to the buffer
// for saving on the thread
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResolvedLocation {
buffer: Entity<Buffer>,
position: Anchor,
}
impl From<&ResolvedLocation> for AgentLocation {
fn from(value: &ResolvedLocation) -> Self {
Self {
buffer: value.buffer.downgrade(),
position: value.position,
}
}
}
#[derive(Debug)]
pub enum ToolCallStatus {
/// The tool call hasn't started running yet, but we start showing it to
@@ -1393,35 +1407,46 @@ impl AcpThread {
let task = tool_call.resolve_locations(project, cx);
cx.spawn(async move |this, cx| {
let resolved_locations = task.await;
this.update(cx, |this, cx| {
let project = this.project.clone();
for location in resolved_locations.iter().flatten() {
this.shared_buffers
.insert(location.buffer.clone(), location.buffer.read(cx).snapshot());
}
let Some((ix, tool_call)) = this.tool_call_mut(&id) else {
return;
};
if let Some(Some(location)) = resolved_locations.last() {
project.update(cx, |project, cx| {
if let Some(agent_location) = project.agent_location() {
let should_ignore = agent_location.buffer == location.buffer
&& location
.buffer
.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let old_position =
agent_location.position.to_point(&snapshot);
let new_position = location.position.to_point(&snapshot);
// ignore this so that when we get updates from the edit tool
// the position doesn't reset to the startof line
old_position.row == new_position.row
&& old_position.column > new_position.column
})
.ok()
.unwrap_or_default();
if !should_ignore {
project.set_agent_location(Some(location.clone()), cx);
}
let should_ignore = if let Some(agent_location) = project
.agent_location()
.filter(|agent_location| agent_location.buffer == location.buffer)
{
let snapshot = location.buffer.read(cx).snapshot();
let old_position = agent_location.position.to_point(&snapshot);
let new_position = location.position.to_point(&snapshot);
// ignore this so that when we get updates from the edit tool
// the position doesn't reset to the startof line
old_position.row == new_position.row
&& old_position.column > new_position.column
} else {
false
};
if !should_ignore {
project.set_agent_location(Some(location.into()), cx);
}
});
}
let resolved_locations = resolved_locations
.iter()
.map(|l| l.as_ref().map(|l| AgentLocation::from(l)))
.collect::<Vec<_>>();
if tool_call.resolved_locations != resolved_locations {
tool_call.resolved_locations = resolved_locations;
cx.emit(AcpThreadEvent::EntryUpdated(ix));

View File

@@ -1,11 +1,16 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CODEX_NAME;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
#[derive(Clone)]
pub struct Codex;
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -278,7 +278,7 @@ pub struct AcpThreadView {
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
collapsed_tool_calls: HashSet<acp::ToolCallId>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
@@ -292,6 +292,8 @@ 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 {
@@ -394,6 +396,10 @@ 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(),
@@ -419,7 +425,7 @@ impl AcpThreadView {
thread_error: None,
thread_feedback: Default::default(),
auth_task: None,
collapsed_tool_calls: HashSet::default(),
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
editing_message: None,
edits_expanded: false,
@@ -436,6 +442,8 @@ 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,
}
}
@@ -954,17 +962,17 @@ impl AcpThreadView {
) {
match &event.view_event {
ViewEvent::NewDiff(tool_call_id) => {
if !AgentSettings::get_global(cx).expand_edit_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
if AgentSettings::get_global(cx).expand_edit_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::NewTerminal(tool_call_id) => {
if !AgentSettings::get_global(cx).expand_terminal_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
if AgentSettings::get_global(cx).expand_terminal_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
self.collapsed_tool_calls.insert(tool_call_id.clone());
self.expanded_tool_calls.remove(tool_call_id);
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
if let Some(thread) = self.thread()
@@ -1045,9 +1053,6 @@ impl AcpThreadView {
return;
};
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let connection = thread.read(cx).connection().clone();
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
@@ -1058,6 +1063,9 @@ impl AcpThreadView {
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
@@ -2119,7 +2127,7 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let tool_output_display =
if is_open {
@@ -2269,9 +2277,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.collapsed_tool_calls.insert(id.clone());
this.expanded_tool_calls.remove(&id);
} else {
this.collapsed_tool_calls.remove(&id);
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
@@ -2473,7 +2481,7 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.on_click(cx.listener({
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
this.collapsed_tool_calls.insert(tool_call_id.clone());
this.expanded_tool_calls.remove(&tool_call_id);
cx.notify();
}
})),
@@ -2751,7 +2759,7 @@ impl AcpThreadView {
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex()
.id(header_id)
@@ -2886,9 +2894,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.collapsed_tool_calls.insert(id.clone());
this.expanded_tool_calls.remove(&id);
} else {
this.collapsed_tool_calls.remove(&id);
this.expanded_tool_calls.insert(id.clone());
}
}
})),
@@ -5025,6 +5033,49 @@ 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),
@@ -5512,6 +5563,16 @@ 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

@@ -222,12 +222,11 @@ enum WhichFontSize {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
Zed,
NativeAgent,
TextThread,
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
@@ -237,8 +236,7 @@ pub enum AgentType {
impl AgentType {
fn label(&self) -> SharedString {
match self {
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Codex => "Codex".into(),
@@ -248,7 +246,7 @@ impl AgentType {
fn icon(&self) -> Option<IconName> {
match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Codex => Some(IconName::AiOpenAi),
@@ -813,7 +811,7 @@ impl AgentPanel {
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Default, Serialize, Deserialize)]
#[derive(Serialize, Deserialize)]
struct LastUsedExternalAgent {
agent: crate::ExternalAgent,
}
@@ -854,18 +852,18 @@ impl AgentPanel {
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.unwrap_or_default()
.agent
.map(|agent| agent.agent)
.unwrap_or(ExternalAgent::NativeAgent)
}
}
};
if !loading {
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
}
let server = ext_agent.server(fs, history);
if !loading {
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
}
this.update_in(cx, |this, window, cx| {
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
@@ -1345,15 +1343,6 @@ 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,10 +161,9 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ExternalAgent {
#[default]
pub enum ExternalAgent {
Gemini,
ClaudeCode,
Codex,
@@ -184,13 +183,13 @@ fn placeholder_command() -> AgentServerCommand {
}
impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Codex => "codex",
Self::Custom { .. } => "custom",
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,
}
}

View File

@@ -84,10 +84,32 @@ impl ZedAiOnboarding {
self
}
fn render_dismiss_button(&self) -> Option<AnyElement> {
self.dismiss_onboarding.as_ref().map(|dismiss_callback| {
let callback = dismiss_callback.clone();
h_flex()
.absolute()
.top_0()
.right_0()
.child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding",);
callback(window, cx)
}),
)
.into_any_element()
})
}
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
.relative()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(
@@ -109,6 +131,7 @@ impl ZedAiOnboarding {
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -180,27 +203,7 @@ impl ZedAiOnboarding {
)
.child(PlanDefinitions.free_plan(is_v2)),
)
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.child(
v_flex()
.mt_2()
@@ -245,26 +248,7 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_trial(is_v2, false))
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -278,26 +262,7 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_plan(is_v2, false))
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
let callback = dismiss_callback.clone();
this.child(
h_flex().absolute().top_0().right_0().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click(move |_, window, cx| {
telemetry::event!(
"Banner Dismissed",
source = "AI Onboarding",
);
callback(window, cx)
}),
),
)
},
)
.children(self.render_dismiss_button())
.into_any_element()
}
}

View File

@@ -2703,7 +2703,7 @@ async fn test_lsp_pull_diagnostics(
let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
smol::channel::bounded::<()>(1);
let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
"workspace/diagnostic-{}-1",
"workspace/diagnostic/{}/1",
fake_language_server.server.server_id()
));
let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use collections::HashMap;
use gpui::{Entity, WeakEntity};
use gpui::{Corner, Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
@@ -211,6 +211,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
@@ -322,6 +323,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),

View File

@@ -594,7 +594,11 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
pub fn remove_inlays_for_excerpts(
&mut self,
excerpts_removed: &[ExcerptId],
cx: &mut Context<Self>,
) {
let to_remove = self
.inlay_map
.current_inlays()
@@ -606,7 +610,7 @@ impl DisplayMap {
}
})
.collect::<Vec<_>>();
self.inlay_map.splice(&to_remove, Vec::new());
self.splice_inlays(&to_remove, Vec::new(), cx);
}
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {

View File

@@ -3899,6 +3899,9 @@ impl Editor {
}
})
.collect::<Vec<_>>();
if selection_ranges.is_empty() {
return;
}
let ranges = match columnar_state {
ColumnarSelectionState::FromMouse { .. } => {
@@ -5288,8 +5291,8 @@ impl Editor {
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, _| {
display_map.remove_inlays_for_excerpts(&excerpts_removed)
self.display_map.update(cx, |display_map, cx| {
display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
});
return;
}

View File

@@ -651,7 +651,6 @@ impl EditorElement {
fn mouse_left_down(
editor: &mut Editor,
event: &MouseDownEvent,
hovered_hunk: Option<Range<Anchor>>,
position_map: &PositionMap,
line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
window: &mut Window,
@@ -667,7 +666,20 @@ impl EditorElement {
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
if let Some(hovered_hunk) = hovered_hunk {
if let Some(hovered_hunk) =
position_map
.display_hunks
.iter()
.find_map(|(hunk, hunk_hitbox)| match hunk {
DisplayDiffHunk::Folded { .. } => None,
DisplayDiffHunk::Unfolded {
multi_buffer_range, ..
} => hunk_hitbox
.as_ref()
.is_some_and(|hitbox| hitbox.is_hovered(window))
.then(|| multi_buffer_range.clone()),
})
{
editor.toggle_single_diff_hunk(hovered_hunk, cx);
cx.notify();
return;
@@ -7247,26 +7259,6 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let diff_hunk_range =
layout
.display_hunks
.iter()
.find_map(|(hunk, hunk_hitbox)| match hunk {
DisplayDiffHunk::Folded { .. } => None,
DisplayDiffHunk::Unfolded {
multi_buffer_range, ..
} => {
if hunk_hitbox
.as_ref()
.map(|hitbox| hitbox.is_hovered(window))
.unwrap_or(false)
{
Some(multi_buffer_range.clone())
} else {
None
}
}
});
let line_numbers = layout.line_numbers.clone();
move |event: &MouseDownEvent, phase, window, cx| {
@@ -7283,7 +7275,6 @@ impl EditorElement {
Self::mouse_left_down(
editor,
event,
diff_hunk_range.clone(),
&position_map,
line_numbers.as_ref(),
window,

View File

@@ -493,22 +493,15 @@ pub fn show_link_definition(
}
let trigger_anchor = trigger_point.anchor();
let Some((buffer, buffer_position)) = editor
.buffer
.read(cx)
.text_anchor_for_position(*trigger_anchor, cx)
else {
let anchor = snapshot.buffer_snapshot().anchor_before(*trigger_anchor);
let Some(buffer) = editor.buffer().read(cx).buffer_for_anchor(anchor, cx) else {
return;
};
let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(*trigger_anchor, cx)
else {
return;
};
let Anchor {
excerpt_id,
text_anchor,
..
} = anchor;
let same_kind = hovered_link_state.preferred_kind == preferred_kind
|| hovered_link_state
.links
@@ -538,7 +531,7 @@ pub fn show_link_definition(
async move {
let result = match &trigger_point {
TriggerPoint::Text(_) => {
if let Some((url_range, url)) = find_url(&buffer, buffer_position, cx.clone()) {
if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
this.read_with(cx, |_, _| {
let range = maybe!({
let start =
@@ -550,7 +543,7 @@ pub fn show_link_definition(
})
.ok()
} else if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), buffer_position, cx).await
find_file(&buffer, project.clone(), text_anchor, cx).await
{
let range = maybe!({
let start =
@@ -562,7 +555,7 @@ pub fn show_link_definition(
Some((range, vec![HoverLink::File(filename)]))
} else if let Some(provider) = provider {
let task = cx.update(|_, cx| {
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
provider.definitions(&buffer, text_anchor, preferred_kind, cx)
})?;
if let Some(task) = task {
task.await.ok().flatten().map(|definition_result| {

View File

@@ -610,21 +610,32 @@ impl<'a> MutableSelectionsCollection<'a> {
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
T: ToOffset + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let mut selections = selections
.into_iter()
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
.map(|mut selection| {
if selection.start > selection.end {
mem::swap(&mut selection.start, &mut selection.end);
selection.reversed = true
}
selection
})
.collect::<Vec<_>>();
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
if selections[i].start <= selections[i - 1].end {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
if selections[i - 1].end < removed.end {
selections[i - 1].end = removed.end;
}
} else {
@@ -968,13 +979,10 @@ impl DerefMut for MutableSelectionsCollection<'_> {
}
}
fn selection_to_anchor_selection<T>(
selection: Selection<T>,
fn selection_to_anchor_selection(
selection: Selection<usize>,
buffer: &MultiBufferSnapshot,
) -> Selection<Anchor>
where
T: ToOffset + Ord,
{
) -> Selection<Anchor> {
let end_bias = if selection.start == selection.end {
Bias::Right
} else {
@@ -1012,7 +1020,7 @@ fn resolve_selections_point<'a>(
})
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
fn resolve_selections_display<'a>(
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
map: &'a DisplaySnapshot,
@@ -1044,7 +1052,7 @@ fn resolve_selections_display<'a>(
coalesce_selections(selections)
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
pub(crate) fn resolve_selections<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,

View File

@@ -669,7 +669,7 @@ impl PickerDelegate for OpenPathDelegate {
) -> Option<Self::ListItem> {
let settings = FileFinderSettings::get_global(cx);
let candidate = self.get_entry(ix)?;
let match_positions = match &self.directory_state {
let mut match_positions = match &self.directory_state {
DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
DirectoryState::Create { user_input, .. } => {
if let Some(user_input) = user_input {
@@ -710,29 +710,38 @@ impl PickerDelegate for OpenPathDelegate {
});
match &self.directory_state {
DirectoryState::List { parent_path, .. } => Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(
if parent_path == &self.prompt_root {
format!("{}{}", self.prompt_root, candidate.path.string)
} else if is_current_dir_candidate {
"open this directory".to_string()
} else {
candidate.path.string
},
DirectoryState::List { parent_path, .. } => {
let (label, indices) = if *parent_path == self.prompt_root {
match_positions.iter_mut().for_each(|position| {
*position += self.prompt_root.len();
});
(
format!("{}{}", self.prompt_root, candidate.path.string),
match_positions,
)),
),
)
} else if is_current_dir_candidate {
("open this directory".to_string(), vec![])
} else {
(candidate.path.string, match_positions)
};
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.start_slot::<Icon>(file_icon)
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(label, indices)),
)
}
DirectoryState::Create {
parent_path,
user_input,
..
} => {
let (label, delta) = if parent_path == &self.prompt_root {
let (label, delta) = if *parent_path == self.prompt_root {
match_positions.iter_mut().for_each(|position| {
*position += self.prompt_root.len();
});
(
format!("{}{}", self.prompt_root, candidate.path.string),
self.prompt_root.len(),
@@ -740,10 +749,10 @@ impl PickerDelegate for OpenPathDelegate {
} else {
(candidate.path.string.clone(), 0)
};
let label_len = label.len();
let label_with_highlights = match user_input {
Some(user_input) => {
let label_len = label.len();
if user_input.file.string == candidate.path.string {
if user_input.exists {
let label = if user_input.is_dir {
@@ -772,20 +781,10 @@ impl PickerDelegate for OpenPathDelegate {
.into_any_element()
}
} else {
let mut highlight_positions = match_positions;
highlight_positions.iter_mut().for_each(|position| {
*position += delta;
});
HighlightedLabel::new(label, highlight_positions).into_any_element()
HighlightedLabel::new(label, match_positions).into_any_element()
}
}
None => {
let mut highlight_positions = match_positions;
highlight_positions.iter_mut().for_each(|position| {
*position += delta;
});
HighlightedLabel::new(label, highlight_positions).into_any_element()
}
None => HighlightedLabel::new(label, match_positions).into_any_element(),
};
Some(

View File

@@ -228,7 +228,7 @@ impl PickerDelegate for PickerPromptDelegate {
let highlights: Vec<_> = hit
.positions
.iter()
.filter(|index| index < &&self.max_match_length)
.filter(|&&index| index < self.max_match_length)
.copied()
.collect();

View File

@@ -1,6 +1,6 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
struct HelloWorld {}
@@ -71,6 +71,12 @@ impl Render for HelloWorld {
.child("100%"),
),
)
.child(div().flex().gap_2().justify_between().child(
StyledText::new("ABCD").with_highlights([
(0..1, FontWeight::EXTRA_BOLD.into()),
(2..3, FontStyle::Italic.into()),
]),
))
}
}

View File

@@ -449,11 +449,12 @@ impl MacTextSystemState {
// to prevent core text from forming ligatures between them
let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id);
let n_zwnjs = self.zwnjs_scratch_space.len();
let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16;
let n_zwnjs = self.zwnjs_scratch_space.len(); // from previous loop
let utf16_start = string.char_len(); // insert at end of string
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0));
// note: replace_str may silently ignore codepoints it dislikes (e.g., BOM at start of string)
string.replace_str(&CFString::new(text), CFRange::init(utf16_start, 0));
if needs_zwnj {
let zwnjs_pos = string.char_len();
self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize));
@@ -462,10 +463,9 @@ impl MacTextSystemState {
CFRange::init(zwnjs_pos, 0),
);
}
let utf16_end = string.char_len() as usize;
let utf16_end = string.char_len();
let cf_range =
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start);
let font = &self.fonts[run.font_id.0];
let font_metrics = font.metrics();
@@ -548,10 +548,12 @@ impl MacTextSystemState {
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
struct StringIndexConverter<'a> {
text: &'a str,
/// Index in UTF-8 bytes
utf8_ix: usize,
/// Index in UTF-16 code units
utf16_ix: usize,
}
@@ -732,6 +734,25 @@ mod tests {
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
let line = "\u{feff}ab";
let font_runs = &[
FontRun {
len: "\u{feff}".len(),
font_id,
},
FontRun {
len: "ab".len(),
font_id,
},
];
let layout = fonts.layout_line(line, px(16.), font_runs);
assert_eq!(layout.len, line.len());
assert_eq!(layout.runs.len(), 1);
assert_eq!(layout.runs[0].glyphs.len(), 2);
// There's no glyph for \u{feff}
assert_eq!(layout.runs[0].glyphs[0].id, GlyphId(68u32)); // a
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
}
#[test]

View File

@@ -530,8 +530,18 @@ impl WindowsWindowInner {
};
let scale_factor = lock.scale_factor;
let wheel_scroll_amount = match modifiers.shift {
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
true => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars
}
false => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_lines
}
};
drop(lock);
@@ -574,7 +584,11 @@ impl WindowsWindowInner {
return Some(1);
};
let scale_factor = lock.scale_factor;
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
let wheel_scroll_chars = self
.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars;
drop(lock);
let wheel_distance =
@@ -707,11 +721,8 @@ 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
.state
.borrow()
.system_settings
.auto_hide_taskbar_position
&& let Some(ref taskbar_position) =
self.system_settings.borrow().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
@@ -1101,9 +1112,11 @@ 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,7 +51,6 @@ 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>,
@@ -66,6 +65,7 @@ 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,7 +115,6 @@ 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;
@@ -138,7 +137,6 @@ impl WindowsWindowState {
hovered,
renderer,
click_state,
system_settings,
current_cursor,
nc_button_pressed,
display,
@@ -231,6 +229,7 @@ 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)),
}))
}
@@ -644,10 +643,12 @@ 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 = if btn.is_cancel() {
IDCANCEL.0
} else {
index as i32 - 100
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,
};
button_id_map.push(button_id);
buttons.push(TASKDIALOG_BUTTON {
@@ -665,11 +666,11 @@ impl PlatformWindow for WindowsWindow {
.context("unable to create task dialog")
.log_err();
let clicked = button_id_map
.iter()
.position(|&button_id| button_id == res)
.unwrap();
let _ = done_tx.send(clicked);
if let Some(clicked) =
button_id_map.iter().position(|&button_id| button_id == res)
{
let _ = done_tx.send(clicked);
}
}
})
.detach();

View File

@@ -424,7 +424,6 @@ impl WindowTextSystem {
font_runs.clear();
let line_end = line_start + line_text.len();
let mut last_font: Option<FontId> = None;
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
let mut run_start = line_start;
while run_start < line_end {
@@ -453,14 +452,13 @@ impl WindowTextSystem {
true
};
let font_id = self.resolve_font(&run.font);
if let Some(font_run) = font_runs.last_mut()
&& Some(font_run.font_id) == last_font
&& font_id == font_run.font_id
&& !decoration_changed
{
font_run.len += run_len_within_line;
} else {
let font_id = self.resolve_font(&run.font);
last_font = Some(font_id);
font_runs.push(FontRun {
len: run_len_within_line,
font_id,

View File

@@ -142,6 +142,8 @@ pub struct LanguageSettings {
pub auto_indent_on_paste: bool,
/// Controls how the editor handles the autoclosed characters.
pub always_treat_brackets_as_autoclosed: bool,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
/// Whether to perform linked edits
pub linked_edits: bool,
/// Task configuration for this language.
@@ -576,6 +578,7 @@ impl settings::Settings for AllLanguageSettings {
always_treat_brackets_as_autoclosed: settings
.always_treat_brackets_as_autoclosed
.unwrap(),
code_actions_on_format: settings.code_actions_on_format.unwrap(),
linked_edits: settings.linked_edits.unwrap(),
tasks: LanguageTaskSettings {
variables: tasks.variables.unwrap_or_default(),

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.len());
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
let filter_range = completion
.filter_text
.as_deref()
@@ -246,7 +246,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.len()),
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -267,7 +267,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.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -288,7 +288,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.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -310,7 +310,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.len()),
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@@ -332,7 +332,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.len()),
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text

View File

@@ -57,29 +57,65 @@ impl RustLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
#[cfg(target_os = "linux")]
enum LibcType {
Gnu,
Musl,
}
impl RustLspAdapter {
#[cfg(target_os = "linux")]
fn build_arch_server_name_linux() -> String {
enum LibcType {
Gnu,
Musl,
async fn determine_libc_type() -> LibcType {
use futures::pin_mut;
use smol::process::Command;
async fn from_ldd_version() -> Option<LibcType> {
let ldd_output = Command::new("ldd").arg("--version").output().await.ok()?;
let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
Some(LibcType::Gnu)
} else if ldd_version.contains("musl") {
Some(LibcType::Musl)
} else {
None
}
}
let has_musl = std::fs::exists(&format!("/lib/ld-musl-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
let has_gnu = std::fs::exists(&format!("/lib/ld-linux-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
if let Some(libc_type) = from_ldd_version().await {
return libc_type;
}
let libc_type = match (has_musl, has_gnu) {
let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
// defaulting to gnu because nix doesn't have /lib files due to not following FHS
return LibcType::Gnu;
};
let dir_entries = dir_entries.filter_map(async move |e| e.ok());
pin_mut!(dir_entries);
let mut has_musl = false;
let mut has_gnu = false;
while let Some(entry) = dir_entries.next().await {
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name.starts_with("ld-musl-") {
has_musl = true;
} else if file_name.starts_with("ld-linux-") {
has_gnu = true;
}
}
match (has_musl, has_gnu) {
(true, _) => LibcType::Musl,
(_, true) => LibcType::Gnu,
_ => {
// defaulting to gnu because nix doesn't have either of those files due to not following FHS
LibcType::Gnu
}
};
_ => LibcType::Gnu,
}
}
let libc = match libc_type {
#[cfg(target_os = "linux")]
async fn build_arch_server_name_linux() -> String {
let libc = match Self::determine_libc_type().await {
LibcType::Musl => "musl",
LibcType::Gnu => "gnu",
};
@@ -87,7 +123,7 @@ impl RustLspAdapter {
format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
}
fn build_asset_name() -> String {
async fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
AssetKind::Gz => "gz",
@@ -95,7 +131,7 @@ impl RustLspAdapter {
};
#[cfg(target_os = "linux")]
let arch_server_name = Self::build_arch_server_name_linux();
let arch_server_name = Self::build_arch_server_name_linux().await;
#[cfg(not(target_os = "linux"))]
let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
@@ -447,7 +483,7 @@ impl LspInstaller for RustLspAdapter {
delegate.http_client(),
)
.await?;
let asset_name = Self::build_asset_name();
let asset_name = Self::build_asset_name().await;
let asset = release
.assets
.into_iter()

View File

@@ -9,7 +9,9 @@ use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
use language::LanguageRegistry;
use markup5ever_rcdom::RcDom;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec};
use std::{
cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
};
pub async fn parse_markdown(
markdown_input: &str,
@@ -407,6 +409,9 @@ impl<'a> MarkdownParser<'a> {
if let Some(mut image) = image.take() {
if !text.is_empty() {
image.set_alt_text(std::mem::take(&mut text).into());
mem::take(&mut highlights);
mem::take(&mut region_ranges);
mem::take(&mut regions);
}
markdown_text_like.push(MarkdownParagraphChunk::Image(image));
}
@@ -1275,17 +1280,40 @@ mod tests {
panic!("Expected a paragraph");
};
assert_eq!(
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..111,
link: Link::Web {
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
},
alt_text: Some("test".into()),
height: None,
width: None,
},)
);
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..111,
link: Link::Web {
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
},
alt_text: Some("test".into()),
height: None,
width: None,
},)
);
}
#[gpui::test]
async fn test_image_alt_text() {
let parsed = parse("[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)\n ").await;
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
text
} else {
panic!("Expected a paragraph");
};
assert_eq!(
paragraph[0],
MarkdownParagraphChunk::Image(Image {
source_range: 0..142,
link: Link::Web {
url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
},
alt_text: Some("Zed".into()),
height: None,
width: None,
},)
);
}
#[gpui::test]

View File

@@ -118,8 +118,8 @@ pub(crate) mod m_2025_10_03 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_10_10 {
pub(crate) mod m_2025_10_16 {
mod settings;
pub(crate) use settings::remove_code_actions_on_format;
pub(crate) use settings::restore_code_actions_on_format;
}

View File

@@ -1,70 +0,0 @@
use anyhow::Result;
use serde_json::Value;
pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
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());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
remove_code_actions_on_format_inner(language, &path)?;
}
}
Ok(())
}
fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else {
return Ok(());
};
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
anyhow::ensure!(
code_actions_on_format.is_object(),
r#"The `code_actions_on_format` setting is deprecated, but it is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
fmt_path(path, "code_actions_on_format"),
);
let code_actions_map = code_actions_on_format.as_object().unwrap();
let mut code_actions = vec![];
for (code_action, code_action_enabled) in code_actions_map {
if code_action_enabled.as_bool().map_or(false, |b| !b) {
continue;
}
code_actions.push(code_action.clone());
}
let mut formatter_array = vec![];
if let Some(formatter) = obj.get("formatter") {
if let Some(array) = formatter.as_array() {
formatter_array = array.clone();
} else {
formatter_array.insert(0, formatter.clone());
}
};
let found_code_actions = !code_actions.is_empty();
formatter_array.splice(
0..0,
code_actions
.into_iter()
.map(|code_action| serde_json::json!({"code_action": code_action})),
);
obj.remove("code_actions_on_format");
if found_code_actions {
obj.insert("formatter".to_string(), Value::Array(formatter_array));
}
Ok(())
}

View File

@@ -0,0 +1,71 @@
use anyhow::Result;
use serde_json::Value;
use crate::patterns::migrate_language_setting;
pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
migrate_language_setting(value, restore_code_actions_on_format_inner)
}
fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let code_actions_on_format = obj
.get("code_actions_on_format")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else {
anyhow::bail!(
r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
fmt_path(path, "code_actions_on_format"),
);
};
let Some(formatter) = obj.get("formatter") else {
return Ok(());
};
let formatter_array = if let Some(array) = formatter.as_array() {
array.clone()
} else {
vec![formatter.clone()]
};
if formatter_array.is_empty() {
return Ok(());
}
let mut code_action_formatters = Vec::new();
for formatter in formatter_array {
let Some(code_action) = formatter.get("code_action") else {
return Ok(());
};
let Some(code_action_name) = code_action.as_str() else {
anyhow::bail!(
r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#,
fmt_path(path, "formatter"),
);
};
code_action_formatters.push(code_action_name.to_string());
}
code_actions_map.extend(
code_action_formatters
.into_iter()
.rev()
.map(|code_action| (code_action, Value::Bool(true))),
);
obj.remove("formatter");
obj.insert(
"code_actions_on_format".into(),
Value::Object(code_actions_map),
);
Ok(())
}

View File

@@ -213,7 +213,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_10_03::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_03,
),
MigrationType::Json(migrations::m_2025_10_10::remove_code_actions_on_format),
MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
];
run_migrations(text, migrations)
}
@@ -367,6 +367,7 @@ mod tests {
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
#[track_caller]
fn assert_migrate_settings(input: &str, output: Option<&str>) {
let migrated = migrate_settings(input).unwrap();
assert_migrated_correctly(migrated, output);
@@ -1341,7 +1342,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_array() {
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": [
{
@@ -1368,7 +1373,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_object() {
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": {
"code_actions": {
@@ -1500,7 +1509,11 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
{
assert_migrate_settings(
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
&r#"{
"formatter": {
"code_actions": {
@@ -1916,300 +1929,109 @@ mod tests {
}
#[test]
fn test_code_actions_on_format_migration_basic() {
fn test_restore_code_actions_on_format() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": true
"formatter": {
"code_action": "foo"
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
{
"code_action": "source.fixAll"
}
]
"code_actions_on_format": {
"foo": true
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_filters_false_values() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"a": true,
"b": false,
"c": true
}
"formatter": [
{ "code_action": "foo" },
"auto"
]
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "a"
},
{
"code_action": "c"
}
]
}
"#
.unindent(),
),
None,
);
}
#[test]
fn test_code_actions_on_format_migration_with_existing_formatter_object() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"formatter": "prettier",
"code_actions_on_format": {
"source.organizeImports": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
"prettier"
]
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_with_existing_formatter_array() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": ["prettier", {"language_server": "eslint"}],
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": true
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.organizeImports"
},
{
"code_action": "source.fixAll"
},
"prettier",
{
"language_server": "eslint"
}
]
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_in_languages() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"languages": {
"JavaScript": {
"code_actions_on_format": {
"source.fixAll.eslint": true
}
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
}
}
}
}"#
.unindent(),
Some(
&r#"{
"languages": {
"JavaScript": {
"formatter": [
{
"code_action": "source.fixAll.eslint"
}
]
},
"Go": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_in_languages_with_existing_formatter() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"languages": {
"JavaScript": {
"formatter": "prettier",
"code_actions_on_format": {
"source.fixAll.eslint": true,
"source.organizeImports": false
}
}
}
}"#
.unindent(),
Some(
&r#"{
"languages": {
"JavaScript": {
"formatter": [
{
"code_action": "source.fixAll.eslint"
},
"prettier"
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_mixed_global_and_languages() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": "prettier",
"code_actions_on_format": {
"source.fixAll": true
},
"languages": {
"Rust": {
"formatter": "rust-analyzer",
"code_actions_on_format": {
"source.organizeImports": true
}
"formatter": {
"code_action": "foo"
},
"Python": {
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": false
}
"code_actions_on_format": {
"bar": true,
"baz": false
}
}
}"#
.unindent(),
Some(
&r#"{
"formatter": [
{
"code_action": "source.fixAll"
},
"prettier"
],
"languages": {
"Rust": {
"formatter": [
{
"code_action": "source.organizeImports"
},
"rust-analyzer"
]
},
"Python": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
"code_actions_on_format": {
"foo": true,
"bar": true,
"baz": false
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_no_migration_when_not_present() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"formatter": ["prettier"]
"formatter": [
{ "code_action": "foo" },
{ "code_action": "qux" },
],
"code_actions_on_format": {
"bar": true,
"baz": false
}
}"#
.unindent(),
Some(
&r#"{
"code_actions_on_format": {
"foo": true,
"qux": true,
"bar": true,
"baz": false
}
}"#
.unindent(),
),
);
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
)],
&r#"{
"formatter": [],
"code_actions_on_format": {
"bar": true,
"baz": false
}
}"#
.unindent(),
None,
);
}
#[test]
fn test_code_actions_on_format_migration_all_false_values() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"code_actions_on_format": {
"a": false,
"b": false
},
"formatter": "prettier"
}"#
.unindent(),
Some(
&r#"{
"formatter": "prettier"
}"#
.unindent(),
),
);
}
}

View File

@@ -10,4 +10,5 @@ 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,3 +108,24 @@ 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

@@ -6165,22 +6165,20 @@ impl MultiBufferSnapshot {
) -> SmallVec<[Locator; 1]> {
let mut sorted_ids = ids.into_iter().collect::<SmallVec<[_; 1]>>();
sorted_ids.sort_unstable();
sorted_ids.dedup();
let mut locators = SmallVec::new();
while sorted_ids.last() == Some(&ExcerptId::max()) {
sorted_ids.pop();
if let Some(mapping) = self.excerpt_ids.last() {
locators.push(mapping.locator.clone());
}
locators.push(Locator::max());
}
let mut sorted_ids = sorted_ids.into_iter().dedup().peekable();
if sorted_ids.peek() == Some(&ExcerptId::min()) {
sorted_ids.next();
if let Some(mapping) = self.excerpt_ids.first() {
locators.push(mapping.locator.clone());
}
}
let mut sorted_ids = sorted_ids.into_iter().peekable();
locators.extend(
sorted_ids
.peeking_take_while(|excerpt| *excerpt == ExcerptId::min())
.map(|_| Locator::min()),
);
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(());
for id in sorted_ids {

View File

@@ -577,7 +577,6 @@ 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>,
@@ -649,12 +648,10 @@ 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()
.filter(|_| channel == "latest");
.ok();
if let Some(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
@@ -663,7 +660,6 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
)
.await
.log_err();
@@ -687,7 +683,6 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
))
.await?
.into()
@@ -736,14 +731,13 @@ 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, channel)])
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.await?;
let version = node_runtime
@@ -886,17 +880,12 @@ 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) {
Some("0.9.0-preview.4".parse().unwrap())
// v0.8.x on Windows has a bug that causes the initialize request to hang forever
Some("0.9.0".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,
@@ -980,7 +969,6 @@ 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

@@ -14,12 +14,13 @@ use super::dap_command::{
TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use base64::Engine;
use collections::{HashMap, HashSet, IndexMap};
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
use dap::messages::Response;
use dap::requests::{Request, RunInTerminal, StartDebugging};
use dap::transport::TcpTransport;
use dap::{
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
SteppingGranularity, StoppedEvent, VariableReference,
@@ -47,12 +48,14 @@ use remote::RemoteClient;
use rpc::ErrorExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::net::TcpListener;
use smol::net::{TcpListener, TcpStream};
use std::any::TypeId;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use std::u64;
use std::{
any::Any,
@@ -63,6 +66,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
use url::Url;
use util::command::new_smol_command;
use util::{ResultExt, debug_panic, maybe};
use worktree::Worktree;
@@ -2768,31 +2772,42 @@ impl Session {
let mut console_output = self.console_output(cx);
let task = cx.spawn(async move |this, cx| {
let (dap_port, _child) =
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
(request.server_port, None)
} else {
let port = {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.context("getting port for DAP")?;
listener.local_addr()?.port()
};
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_port_command(
port,
"localhost".into(),
request.server_port,
)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
(port, Some(child))
};
let forward_ports_process = if remote_client
.read_with(cx, |client, _| client.shares_network_interface())?
{
request.other.insert(
"proxyUri".into(),
format!("127.0.0.1:{}", request.server_port).into(),
);
None
} else {
let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST)
.await
.context("getting port for DAP")?;
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{port}").into());
let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)];
if let Some(value) = request.params.get("url")
&& let Some(url) = value.as_str()
&& let Some(url) = Url::parse(url).ok()
&& let Some(frontend_port) = url.port()
{
port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port));
}
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_ports_command(port_forwards)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
Some(child)
};
let mut companion_process = None;
let companion_port =
@@ -2814,14 +2829,17 @@ impl Session {
}
}
};
this.update(cx, |this, cx| {
this.companion_port = Some(companion_port);
let Some(mut child) = companion_process else {
return;
};
if let Some(stderr) = child.stderr.take() {
let mut background_tasks = Vec::new();
if let Some(mut forward_ports_process) = forward_ports_process {
background_tasks.push(cx.spawn(async move |_| {
forward_ports_process.status().await.log_err();
}));
};
if let Some(mut companion_process) = companion_process {
if let Some(stderr) = companion_process.stderr.take() {
let mut console_output = console_output.clone();
this.background_tasks.push(cx.spawn(async move |_, _| {
background_tasks.push(cx.spawn(async move |_| {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -2835,9 +2853,9 @@ impl Session {
}
}));
}
this.background_tasks.push(cx.spawn({
background_tasks.push(cx.spawn({
let mut console_output = console_output.clone();
async move |_, _| match child.status().await {
async move |_| match companion_process.status().await {
Ok(status) => {
if status.success() {
console_output
@@ -2860,17 +2878,33 @@ impl Session {
.ok();
}
}
}))
})?;
}));
}
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
// TODO pass wslInfo as needed
let companion_address = format!("127.0.0.1:{companion_port}");
let mut companion_started = false;
for _ in 0..10 {
if TcpStream::connect(&companion_address).await.is_ok() {
companion_started = true;
break;
}
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
}
if !companion_started {
console_output
.send("Browser companion failed to start".into())
.await
.ok();
bail!("Browser companion failed to start");
}
let response = http_client
.post_json(
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
&format!("http://{companion_address}/launch-and-attach"),
serde_json::to_string(&request)
.context("serializing request")?
.into(),
@@ -2895,6 +2929,11 @@ impl Session {
}
}
this.update(cx, |this, _| {
this.background_tasks.extend(background_tasks);
this.companion_port = Some(companion_port);
})?;
anyhow::Ok(())
});
self.background_tasks.push(cx.spawn(async move |_, _| {
@@ -2926,15 +2965,16 @@ impl Session {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct LaunchBrowserInCompanionParams {
server_port: u16,
params: HashMap<String, serde_json::Value>,
#[serde(flatten)]
other: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct KillCompanionBrowserParams {
launch_id: u64,

View File

@@ -26,8 +26,8 @@ use language::{
use lsp::{
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
OneOf, RenameOptions, ServerCapabilities,
DiagnosticServerCapabilities, DocumentHighlightKind, LanguageServer, LanguageServerId,
LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities,
};
use serde_json::Value;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
@@ -262,6 +262,9 @@ pub(crate) struct LinkedEditingRange {
#[derive(Clone, Debug)]
pub(crate) struct GetDocumentDiagnostics {
/// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas
/// a server can register multiple diagnostic providers post-mortem.
pub dynamic_caps: DiagnosticServerCapabilities,
pub previous_result_id: Option<String>,
}
@@ -4019,26 +4022,22 @@ impl LspCommand for GetDocumentDiagnostics {
"Get diagnostics"
}
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
server_capabilities
.server_capabilities
.diagnostic_provider
.is_some()
fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
true
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
language_server: &Arc<LanguageServer>,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::DocumentDiagnosticParams> {
let identifier = match language_server.capabilities().diagnostic_provider {
Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
options.diagnostic_options.identifier
let identifier = match &self.dynamic_caps {
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.identifier.clone()
}
None => None,
};
Ok(lsp::DocumentDiagnosticParams {

View File

@@ -14,6 +14,7 @@ pub mod json_language_server_ext;
pub mod log_store;
pub mod lsp_ext_command;
pub mod rust_analyzer_ext;
pub mod vue_language_server_ext;
use crate::{
CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
@@ -70,12 +71,12 @@ use language::{
range_from_lsp, range_to_lsp,
};
use lsp::{
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity,
DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter,
FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture,
MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticServerCapabilities,
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit,
FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename,
FileSystemWatcher, LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary,
LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector,
LspRequestFuture, MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind,
TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, WorkDoneProgressCancelParams,
WorkspaceFolder, notification::DidRenameFiles,
};
@@ -181,6 +182,12 @@ pub struct DocumentDiagnostics {
version: Option<i32>,
}
#[derive(Default)]
struct DynamicRegistrations {
did_change_watched_files: HashMap<String, Vec<FileSystemWatcher>>,
diagnostics: HashMap<Option<String>, DiagnosticServerCapabilities>,
}
pub struct LocalLspStore {
weak: WeakEntity<LspStore>,
worktree_store: Entity<WorktreeStore>,
@@ -198,8 +205,7 @@ pub struct LocalLspStore {
watched_manifest_filenames: HashSet<ManifestName>,
language_server_paths_watched_for_rename:
HashMap<LanguageServerId, RenamePathsWatchedForServer>,
language_server_watcher_registrations:
HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
language_server_dynamic_registrations: HashMap<LanguageServerId, DynamicRegistrations>,
supplementary_language_servers:
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
prettier_store: Entity<PrettierStore>,
@@ -987,6 +993,7 @@ impl LocalLspStore {
})
.detach();
vue_language_server_ext::register_requests(this.clone(), language_server);
json_language_server_ext::register_requests(this.clone(), language_server);
rust_analyzer_ext::register_notifications(this.clone(), language_server);
clangd_ext::register_notifications(this, language_server, adapter);
@@ -1333,6 +1340,32 @@ impl LocalLspStore {
})?;
}
// Formatter for `code_actions_on_format` that runs before
// the rest of the formatters
let mut code_actions_on_format_formatters = None;
let should_run_code_actions_on_format = !matches!(
(trigger, &settings.format_on_save),
(FormatTrigger::Save, &FormatOnSave::Off)
);
if should_run_code_actions_on_format {
let have_code_actions_to_run_on_format = settings
.code_actions_on_format
.values()
.any(|enabled| *enabled);
if have_code_actions_to_run_on_format {
zlog::trace!(logger => "going to run code actions on format");
code_actions_on_format_formatters = Some(
settings
.code_actions_on_format
.iter()
.filter_map(|(action, enabled)| enabled.then_some(action))
.cloned()
.map(Formatter::CodeAction)
.collect::<Vec<_>>(),
);
}
}
let formatters = match (trigger, &settings.format_on_save) {
(FormatTrigger::Save, FormatOnSave::Off) => &[],
(FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
@@ -1340,6 +1373,11 @@ impl LocalLspStore {
}
};
let formatters = code_actions_on_format_formatters
.iter()
.flatten()
.chain(formatters);
for formatter in formatters {
let formatter = if formatter == &Formatter::Auto {
if settings.prettier.allowed {
@@ -3142,7 +3180,7 @@ impl LocalLspStore {
for watcher in watchers {
if let Some((worktree, literal_prefix, pattern)) =
self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
Self::worktree_and_path_for_file_watcher(&worktrees, watcher, cx)
{
worktree.update(cx, |worktree, _| {
if let Some((tree, glob)) =
@@ -3240,7 +3278,6 @@ impl LocalLspStore {
}
fn worktree_and_path_for_file_watcher(
&self,
worktrees: &[Entity<Worktree>],
watcher: &FileSystemWatcher,
cx: &App,
@@ -3288,15 +3325,18 @@ impl LocalLspStore {
language_server_id: LanguageServerId,
cx: &mut Context<LspStore>,
) {
let Some(watchers) = self
.language_server_watcher_registrations
let Some(registrations) = self
.language_server_dynamic_registrations
.get(&language_server_id)
else {
return;
};
let watch_builder =
self.rebuild_watched_paths_inner(language_server_id, watchers.values().flatten(), cx);
let watch_builder = self.rebuild_watched_paths_inner(
language_server_id,
registrations.did_change_watched_files.values().flatten(),
cx,
);
let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx);
self.language_server_watched_paths
.insert(language_server_id, watcher);
@@ -3312,11 +3352,13 @@ impl LocalLspStore {
cx: &mut Context<LspStore>,
) {
let registrations = self
.language_server_watcher_registrations
.language_server_dynamic_registrations
.entry(language_server_id)
.or_default();
registrations.insert(registration_id.to_string(), params.watchers);
registrations
.did_change_watched_files
.insert(registration_id.to_string(), params.watchers);
self.rebuild_watched_paths(language_server_id, cx);
}
@@ -3328,11 +3370,15 @@ impl LocalLspStore {
cx: &mut Context<LspStore>,
) {
let registrations = self
.language_server_watcher_registrations
.language_server_dynamic_registrations
.entry(language_server_id)
.or_default();
if registrations.remove(registration_id).is_some() {
if registrations
.did_change_watched_files
.remove(registration_id)
.is_some()
{
log::info!(
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
@@ -3703,7 +3749,7 @@ impl LspStore {
last_workspace_edits_by_language_server: Default::default(),
language_server_watched_paths: Default::default(),
language_server_paths_watched_for_rename: Default::default(),
language_server_watcher_registrations: Default::default(),
language_server_dynamic_registrations: Default::default(),
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(),
prettier_store,
@@ -4291,7 +4337,7 @@ impl LspStore {
cx: &Context<Self>,
) -> bool
where
F: Fn(&lsp::ServerCapabilities) -> bool,
F: FnMut(&lsp::ServerCapabilities) -> bool,
{
let Some(language) = buffer.read(cx).language().cloned() else {
return false;
@@ -6293,12 +6339,30 @@ impl LspStore {
let buffer_id = buffer.read(cx).remote_id();
if let Some((client, upstream_project_id)) = self.upstream_client() {
let mut suitable_capabilities = None;
// Are we capable for proto request?
let any_server_has_diagnostics_provider = self.check_if_capable_for_proto_request(
&buffer,
|capabilities| {
if let Some(caps) = &capabilities.diagnostic_provider {
suitable_capabilities = Some(caps.clone());
true
} else {
false
}
},
cx,
);
// We don't really care which caps are passed into the request, as they're ignored by RPC anyways.
let Some(dynamic_caps) = suitable_capabilities else {
return Task::ready(Ok(None));
};
assert!(any_server_has_diagnostics_provider);
let request = GetDocumentDiagnostics {
previous_result_id: None,
dynamic_caps,
};
if !self.is_capable_for_proto_request(&buffer, &request, cx) {
return Task::ready(Ok(None));
}
let request_task = client.request_lsp(
upstream_project_id,
LSP_REQUEST_TIMEOUT,
@@ -6313,23 +6377,44 @@ impl LspStore {
Ok(None)
})
} else {
let server_ids = buffer.update(cx, |buffer, cx| {
let servers = buffer.update(cx, |buffer, cx| {
self.language_servers_for_local_buffer(buffer, cx)
.map(|(_, server)| server.server_id())
.map(|(_, server)| server.clone())
.collect::<Vec<_>>()
});
let pull_diagnostics = server_ids
let pull_diagnostics = servers
.into_iter()
.map(|server_id| {
let result_id = self.result_id(server_id, buffer_id, cx);
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetDocumentDiagnostics {
previous_result_id: result_id,
},
cx,
)
.flat_map(|server| {
let result = maybe!({
let local = self.as_local()?;
let server_id = server.server_id();
let providers_with_identifiers = local
.language_server_dynamic_registrations
.get(&server_id)
.into_iter()
.flat_map(|registrations| registrations.diagnostics.values().cloned())
.collect::<Vec<_>>();
Some(
providers_with_identifiers
.into_iter()
.map(|dynamic_caps| {
let result_id = self.result_id(server_id, buffer_id, cx);
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetDocumentDiagnostics {
previous_result_id: result_id,
dynamic_caps,
},
cx,
)
})
.collect::<Vec<_>>(),
)
});
result.unwrap_or_default()
})
.collect::<Vec<_>>();
@@ -8862,14 +8947,17 @@ impl LspStore {
);
}
lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => {
let identifier = token.split_once("id:").map(|(_, id)| id.to_owned());
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = self
.as_local_mut()
.and_then(|local| local.language_servers.get_mut(&language_server_id))
&& let Some(workspace_diagnostics) =
workspace_diagnostics_refresh_tasks.get_mut(&identifier)
{
workspace_refresh_task.progress_tx.try_send(()).ok();
workspace_diagnostics.progress_tx.try_send(()).ok();
self.apply_workspace_diagnostic_report(language_server_id, report, cx)
}
}
@@ -10375,13 +10463,31 @@ impl LspStore {
let workspace_folders = workspace_folders.lock().clone();
language_server.set_workspace_folders(workspace_folders);
let workspace_diagnostics_refresh_tasks = language_server
.capabilities()
.diagnostic_provider
.and_then(|provider| {
let workspace_refresher = lsp_workspace_diagnostics_refresh(
None,
provider.clone(),
language_server.clone(),
cx,
)?;
local
.language_server_dynamic_registrations
.entry(server_id)
.or_default()
.diagnostics
.entry(None)
.or_insert(provider);
Some((None, workspace_refresher))
})
.into_iter()
.collect();
local.language_servers.insert(
server_id,
LanguageServerState::Running {
workspace_refresh_task: lsp_workspace_diagnostics_refresh(
language_server.clone(),
cx,
),
workspace_diagnostics_refresh_tasks,
adapter: adapter.clone(),
server: language_server.clone(),
simulate_disk_based_diagnostics_completion: None,
@@ -11091,13 +11197,15 @@ impl LspStore {
pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) {
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = self
.as_local_mut()
.and_then(|local| local.language_servers.get_mut(&server_id))
{
workspace_refresh_task.refresh_tx.try_send(()).ok();
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
diagnostics.refresh_tx.try_send(()).ok();
}
}
}
@@ -11113,11 +11221,13 @@ impl LspStore {
local.language_server_ids_for_buffer(buffer, cx)
}) {
if let Some(LanguageServerState::Running {
workspace_refresh_task: Some(workspace_refresh_task),
workspace_diagnostics_refresh_tasks,
..
}) = local.language_servers.get_mut(&server_id)
{
workspace_refresh_task.refresh_tx.try_send(()).ok();
for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() {
diagnostics.refresh_tx.try_send(()).ok();
}
}
}
}
@@ -11443,26 +11553,49 @@ impl LspStore {
"textDocument/diagnostic" => {
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.map(serde_json::from_value::<DiagnosticServerCapabilities>)
.transpose()?
{
let state = self
let local = self
.as_local_mut()
.context("Expected LSP Store to be local")?
.context("Expected LSP Store to be local")?;
let state = local
.language_servers
.get_mut(&server_id)
.context("Could not obtain Language Servers state")?;
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = Some(caps);
});
local
.language_server_dynamic_registrations
.get_mut(&server_id)
.and_then(|registrations| {
registrations
.diagnostics
.insert(Some(reg.id.clone()), caps.clone())
});
let mut can_now_provide_diagnostics = false;
if let LanguageServerState::Running {
workspace_refresh_task,
workspace_diagnostics_refresh_tasks,
..
} = state
&& workspace_refresh_task.is_none()
&& let Some(task) = lsp_workspace_diagnostics_refresh(
Some(reg.id.clone()),
caps.clone(),
server.clone(),
cx,
)
{
*workspace_refresh_task =
lsp_workspace_diagnostics_refresh(server.clone(), cx)
workspace_diagnostics_refresh_tasks.insert(Some(reg.id), task);
can_now_provide_diagnostics = true;
}
// We don't actually care about capabilities.diagnostic_provider, but it IS relevant for the remote peer
// to know that there's at least one provider. Otherwise, it will never ask us to issue documentdiagnostic calls on their behalf,
// as it'll think that they're not supported.
if can_now_provide_diagnostics {
server.update_capabilities(|capabilities| {
debug_assert!(capabilities.diagnostic_provider.is_none());
capabilities.diagnostic_provider = Some(caps);
});
}
notify_server_capabilities_updated(&server, cx);
@@ -11625,22 +11758,45 @@ impl LspStore {
notify_server_capabilities_updated(&server, cx);
}
"textDocument/diagnostic" => {
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = None;
});
let state = self
let local = self
.as_local_mut()
.context("Expected LSP Store to be local")?
.context("Expected LSP Store to be local")?;
let state = local
.language_servers
.get_mut(&server_id)
.context("Could not obtain Language Servers state")?;
if let LanguageServerState::Running {
workspace_refresh_task,
..
} = state
let options = local
.language_server_dynamic_registrations
.get_mut(&server_id)
.with_context(|| {
format!("Expected dynamic registration to exist for server {server_id}")
})?.diagnostics
.remove(&Some(unreg.id.clone()))
.with_context(|| format!(
"Attempted to unregister non-existent diagnostic registration with ID {}",
unreg.id)
)?;
let mut has_any_diagnostic_providers_still = true;
if let Some(identifier) = diagnostic_identifier(&options)
&& let LanguageServerState::Running {
workspace_diagnostics_refresh_tasks,
..
} = state
{
_ = workspace_refresh_task.take();
workspace_diagnostics_refresh_tasks.remove(&identifier);
has_any_diagnostic_providers_still =
!workspace_diagnostics_refresh_tasks.is_empty();
}
if !has_any_diagnostic_providers_still {
server.update_capabilities(|capabilities| {
debug_assert!(capabilities.diagnostic_provider.is_some());
capabilities.diagnostic_provider = None;
});
}
notify_server_capabilities_updated(&server, cx);
}
"textDocument/documentColor" => {
@@ -11826,24 +11982,12 @@ fn subscribe_to_binary_statuses(
}
fn lsp_workspace_diagnostics_refresh(
registration_id: Option<String>,
options: DiagnosticServerCapabilities,
server: Arc<LanguageServer>,
cx: &mut Context<'_, LspStore>,
) -> Option<WorkspaceRefreshTask> {
let identifier = match server.capabilities().diagnostic_provider? {
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
if !diagnostic_options.workspace_diagnostics {
return None;
}
diagnostic_options.identifier
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
let diagnostic_options = registration_options.diagnostic_options;
if !diagnostic_options.workspace_diagnostics {
return None;
}
diagnostic_options.identifier
}
};
let identifier = diagnostic_identifier(&options)?;
let (progress_tx, mut progress_rx) = mpsc::channel(1);
let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1);
@@ -11889,7 +12033,14 @@ fn lsp_workspace_diagnostics_refresh(
return;
};
let token = format!("workspace/diagnostic-{}-{}", server.server_id(), requests);
let token = if let Some(identifier) = &registration_id {
format!(
"workspace/diagnostic/{}/{requests}/id:{identifier}",
server.server_id(),
)
} else {
format!("workspace/diagnostic/{}/{requests}", server.server_id())
};
progress_rx.try_recv().ok();
let timer =
@@ -11955,6 +12106,24 @@ fn lsp_workspace_diagnostics_refresh(
})
}
fn diagnostic_identifier(options: &DiagnosticServerCapabilities) -> Option<Option<String>> {
match &options {
lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => {
if !diagnostic_options.workspace_diagnostics {
return None;
}
Some(diagnostic_options.identifier.clone())
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => {
let diagnostic_options = &registration_options.diagnostic_options;
if !diagnostic_options.workspace_diagnostics {
return None;
}
Some(diagnostic_options.identifier.clone())
}
}
}
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
let CompletionSource::BufferWord {
word_range,
@@ -12359,7 +12528,7 @@ pub enum LanguageServerState {
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
workspace_refresh_task: Option<WorkspaceRefreshTask>,
workspace_diagnostics_refresh_tasks: HashMap<Option<String>, WorkspaceRefreshTask>,
},
}

View File

@@ -0,0 +1,124 @@
use anyhow::Context as _;
use gpui::{AppContext, WeakEntity};
use lsp::{LanguageServer, LanguageServerName};
use serde_json::Value;
use crate::LspStore;
struct VueServerRequest;
struct TypescriptServerResponse;
impl lsp::notification::Notification for VueServerRequest {
type Params = Vec<(u64, String, serde_json::Value)>;
const METHOD: &'static str = "tsserver/request";
}
impl lsp::notification::Notification for TypescriptServerResponse {
type Params = Vec<(u64, serde_json::Value)>;
const METHOD: &'static str = "tsserver/response";
}
const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server");
const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls");
const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server");
pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
let language_server_name = language_server.name();
if language_server_name == VUE_SERVER_NAME {
let vue_server_id = language_server.server_id();
language_server
.on_notification::<VueServerRequest, _>({
move |params, cx| {
let lsp_store = lsp_store.clone();
let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| {
this.language_server_for_id(vue_server_id)
}) else {
return;
};
let requests = params;
let target_server = match lsp_store.read_with(cx, |this, _| {
let language_server_id = this
.as_local()
.and_then(|local| {
local.language_server_ids.iter().find_map(|(seed, v)| {
[VTSLS, TS_LS].contains(&seed.name).then_some(v.id)
})
})
.context("Could not find language server")?;
this.language_server_for_id(language_server_id)
.context("language server not found")
}) {
Ok(Ok(server)) => server,
other => {
log::warn!(
"vue-language-server forwarding skipped: {other:?}. \
Returning null tsserver responses"
);
if !requests.is_empty() {
let null_responses = requests
.into_iter()
.map(|(id, _, _)| (id, Value::Null))
.collect::<Vec<_>>();
let _ = vue_server
.notify::<TypescriptServerResponse>(null_responses);
}
return;
}
};
let cx = cx.clone();
for (request_id, command, payload) in requests.into_iter() {
let target_server = target_server.clone();
let vue_server = vue_server.clone();
cx.background_spawn(async move {
let response = target_server
.request::<lsp::request::ExecuteCommand>(
lsp::ExecuteCommandParams {
command: "typescript.tsserverRequest".to_owned(),
arguments: vec![Value::String(command), payload],
..Default::default()
},
)
.await;
let response_body = match response {
util::ConnectionResult::Result(Ok(result)) => match result {
Some(Value::Object(mut map)) => map
.remove("body")
.unwrap_or(Value::Object(map)),
Some(other) => other,
None => Value::Null,
},
util::ConnectionResult::Result(Err(error)) => {
log::warn!(
"typescript.tsserverRequest failed: {error:?} for request {request_id}"
);
Value::Null
}
other => {
log::warn!(
"typescript.tsserverRequest did not return a response: {other:?} for request {request_id}"
);
Value::Null
}
};
if let Err(err) = vue_server
.notify::<TypescriptServerResponse>(vec![(request_id, response_body)])
{
log::warn!(
"Failed to notify vue-language-server of tsserver response: {err:?}"
);
}
})
.detach();
}
}
})
.detach();
}
}

View File

@@ -5,7 +5,7 @@ use std::{
sync::{Arc, atomic::AtomicUsize},
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::{HashMap, HashSet};
use fs::{Fs, copy_recursive};
use futures::{
@@ -1194,6 +1194,16 @@ impl WorktreeStore {
RelPath::from_proto(&envelope.payload.new_path)?,
);
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let Some(entry) = this.entry_for_id(entry_id, cx) else {
bail!("no such entry");
};
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
let new_worktree = this
.worktree_for_id(new_worktree_id, cx)
.context("no such worktree")?;
@@ -1217,6 +1227,15 @@ impl WorktreeStore {
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let worktree = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let Some(entry) = this.entry_for_id(entry_id, cx) else {
bail!("no entry")
};
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
this.worktree_for_entry(entry_id, cx)
.context("worktree not found")
})??;
@@ -1237,6 +1256,18 @@ impl WorktreeStore {
let worktree = this
.worktree_for_entry(entry_id, cx)
.context("no such worktree")?;
let Some((_, project_id)) = this.downstream_client else {
bail!("no downstream client")
};
let entry = worktree
.read(cx)
.entry_for_id(entry_id)
.ok_or_else(|| anyhow!("missing entry"))?;
if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID {
bail!("entry is private")
}
let scan_id = worktree.read(cx).scan_id();
anyhow::Ok((
scan_id,

View File

@@ -64,7 +64,7 @@ use workspace::{
DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry,
SplitDirection, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyTaskExt},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
};
use worktree::CreatedEntry;
use zed_actions::workspace::OpenWithSystem;
@@ -2677,12 +2677,14 @@ impl ProjectPanel {
for task in paste_tasks {
match task {
PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
if let Some(CreatedEntry::Included(entry)) =
task.await.notify_async_err(cx)
{
last_succeed = Some(entry);
}
}
PasteTask::Copy(task) => {
if let Some(Some(entry)) = task.await.log_err() {
if let Some(Some(entry)) = task.await.notify_async_err(cx) {
last_succeed = Some(entry);
}
}

View File

@@ -1385,14 +1385,21 @@ impl RemoteServerProjects {
cx: &mut Context<Self>,
) {
self.update_settings_file(cx, move |setting, _| {
setting
.wsl_connections
.get_or_insert(Default::default())
.push(settings::WslConnection {
distro_name: SharedString::from(connection_options.distro_name),
user: connection_options.user,
let connections = setting.wsl_connections.get_or_insert(Default::default());
let distro_name = SharedString::from(connection_options.distro_name);
let user = connection_options.user;
if !connections
.iter()
.any(|conn| conn.distro_name == distro_name && conn.user == user)
{
connections.push(settings::WslConnection {
distro_name,
user,
projects: BTreeSet::new(),
})
}
});
}

View File

@@ -836,16 +836,14 @@ impl RemoteClient {
connection.build_command(program, args, env, working_dir, port_forward)
}
pub fn build_forward_port_command(
pub fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Some(connection) = self.remote_connection() else {
return Err(anyhow!("no ssh connection"));
};
connection.build_forward_port_command(local_port, host, remote_port)
connection.build_forward_ports_command(forwards)
}
pub fn upload_directory(
@@ -1116,11 +1114,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
remote: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn connection_options(&self) -> RemoteConnectionOptions;
fn path_style(&self) -> PathStyle;
@@ -1551,19 +1547,17 @@ mod fake {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}

View File

@@ -146,19 +146,20 @@ impl RemoteConnection for SshRemoteConnection {
)
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Self { socket, .. } = self;
let mut args = socket.ssh_args();
args.push("-N".into());
for (local_port, host, remote_port) in forwards {
args.push("-L".into());
args.push(format!("{local_port}:{host}:{remote_port}"));
}
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args,
env: Default::default(),
})
}

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, cx)
this.ensure_server_binary(&delegate, release_channel, version, commit, shell, cx)
.await?,
);
log::debug!("Detected WSL environment: {this:#?}");
@@ -163,6 +163,7 @@ impl WslRemoteConnection {
release_channel: ReleaseChannel,
version: SemanticVersion,
commit: Option<AppCommitSha>,
shell: ShellKind,
cx: &mut AsyncApp,
) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
@@ -184,9 +185,13 @@ impl WslRemoteConnection {
paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
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))?;
}
#[cfg(debug_assertions)]
@@ -485,11 +490,9 @@ impl RemoteConnection for WslRemoteConnection {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
_: u16,
_: String,
_: u16,
_: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Err(anyhow!("WSL shares a network interface with the host"))
}

View File

@@ -103,7 +103,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> {
buffer: Vec::new(),
});
env_logger::Builder::from_default_env()
env_logger::Builder::new()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.target(env_logger::Target::Pipe(target))
.format(|buf, record| {
let mut log_record = LogRecord::new(record);

View File

@@ -318,6 +318,11 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub use_on_type_format: Option<bool>,
/// Which code actions to run on save after the formatter.
/// These are not run if formatting is off.
///
/// Default: {} (or {"source.organizeImports": true} for Go).
pub code_actions_on_format: Option<HashMap<String, bool>>,
/// Whether to perform linked edits of associated ranges, if the language server supports it.
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
///

View File

@@ -254,9 +254,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("Fonts"),
SettingsPageItem::SectionHeader("Buffer Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Family",
title: "Font Family",
description: "Font family for editor text",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_family,
@@ -266,7 +266,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Size",
title: "Font Size",
description: "Font size for editor text",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_size,
@@ -276,7 +276,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Weight",
title: "Font Weight",
description: "Font weight for editor text (100-900)",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.buffer_font_weight,
@@ -288,7 +288,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
// todo(settings_ui): This needs custom ui
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Line Height",
title: "Line Height",
description: "Line height for editor text",
field: Box::new(
SettingField {
@@ -303,7 +303,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Font Features",
title: "Font Features",
description: "The OpenType features to enable for rendering in text buffers.",
field: Box::new(
SettingField {
@@ -318,7 +318,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Buffer Font Fallbacks",
title: "Font Fallbacks",
description: "The font fallbacks to use for rendering in text buffers.",
field: Box::new(
SettingField {
@@ -331,8 +331,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("UI Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Family",
title: "Font Family",
description: "Font family for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_family,
@@ -342,7 +343,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Size",
title: "Font Size",
description: "Font size for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_size,
@@ -352,7 +353,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Weight",
title: "Font Weight",
description: "Font weight for UI elements (100-900)",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_weight,
@@ -363,7 +364,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "UI Font Features",
title: "Font Features",
description: "The OpenType features to enable for rendering in UI elements.",
field: Box::new(
SettingField {
@@ -378,7 +379,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "UI Font Fallbacks",
title: "Font Fallbacks",
description: "The font fallbacks to use for rendering in the UI.",
field: Box::new(
SettingField {
@@ -391,8 +392,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("Agent Panel Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel UI Font Size",
title: "UI Font Size",
description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -408,7 +410,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Agent Panel Buffer Font Size",
title: "Buffer Font Size",
description: "Font size for user messages text in the agent panel",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
@@ -419,6 +421,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Cursor"),
SettingsPageItem::SettingItem(SettingItem {
title: "Multi Cursor Modifier",
description: "Modifier key for adding multiple cursors",
@@ -431,7 +434,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Cursor"),
SettingsPageItem::SettingItem(SettingItem {
title: "Cursor Blink",
description: "Whether the cursor blinks in the editor",
@@ -808,9 +810,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Hover"),
SettingsPageItem::SectionHeader("Hover Popover"),
SettingsPageItem::SettingItem(SettingItem {
title: "Hover Popover Enabled",
title: "Enabled",
description: "Show the informational hover box when moving the mouse over symbols in the editor",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.hover_popover_enabled,
@@ -823,7 +825,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
// todo(settings ui): add units to this number input
SettingsPageItem::SettingItem(SettingItem {
title: "Hover Popover Delay",
title: "Delay",
description: "Time to wait in milliseconds before showing the informational hover box",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.hover_popover_delay,
@@ -834,21 +836,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Code Actions & Selection"),
SettingsPageItem::SectionHeader("Drag And Drop Selection"),
SettingsPageItem::SettingItem(SettingItem {
title: "Inline Code Actions",
description: "Show code action button at start of buffer line",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.inline_code_actions,
pick_mut: |settings_content| {
&mut settings_content.editor.inline_code_actions
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Drag And Drop Selection",
title: "Enabled",
description: "Enable drag and drop selection",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -872,7 +862,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Drag And Drop Selection Delay",
title: "Delay",
description: "Delay in milliseconds before drag and drop selection starts",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -1014,6 +1004,18 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Inline Code Actions",
description: "Show code action button at start of buffer line",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.editor.inline_code_actions,
pick_mut: |settings_content| {
&mut settings_content.editor.inline_code_actions
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Scrollbar"),
SettingsPageItem::SettingItem(SettingItem {
title: "Show",
@@ -5414,6 +5416,27 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
metadata: None,
files: USER | LOCAL,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Code Actions On Format",
description: "Additional Code Actions To Run When Formatting",
field: Box::new(
SettingField {
pick: |settings_content| {
language_settings_field(settings_content, |language| {
&language.code_actions_on_format
})
},
pick_mut: |settings_content| {
language_settings_field_mut(settings_content, |language| {
&mut language.code_actions_on_format
})
},
}
.unimplemented(),
),
metadata: None,
files: USER | LOCAL,
}),
SettingsPageItem::SectionHeader("Autoclose"),
SettingsPageItem::SettingItem(SettingItem {
title: "Use Autoclose",

View File

@@ -15,7 +15,7 @@ use heck::ToTitleCase as _;
use project::WorktreeId;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsContent, SettingsStore};
use settings::{Settings, SettingsContent, SettingsStore};
use std::{
any::{Any, TypeId, type_name},
cell::RefCell,
@@ -466,6 +466,13 @@ pub fn open_settings_editor(
// We have to defer this to get the workspace off the stack.
cx.defer(move |cx| {
let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into();
let default_bounds = size(px(900.), px(750.)); // 4:3 Aspect Ratio
let default_rem_size = 16.0;
let scale_factor = current_rem_size / default_rem_size;
let scaled_bounds: gpui::Size<Pixels> = default_bounds.map(|axis| axis * scale_factor);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
@@ -478,8 +485,8 @@ pub fn open_settings_editor(
is_movable: true,
kind: gpui::WindowKind::Floating,
window_background: cx.theme().window_background_appearance(),
window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),
window_min_size: Some(scaled_bounds),
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
..Default::default()
},
|window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
@@ -1695,7 +1702,7 @@ impl SettingsWindow {
};
v_flex()
.w_64()
.w_56()
.p_2p5()
.when(cfg!(target_os = "macos"), |c| c.pt_10())
.h_full()
@@ -2130,7 +2137,7 @@ impl SettingsWindow {
}
return v_flex()
.size_full()
.flex_1()
.pt_6()
.pb_8()
.px_8()

View File

@@ -97,6 +97,7 @@ impl Render for PlatformTitleBar {
})
// this border is to avoid a transparent gap in the rounded corners
.mt(px(-1.))
.mb(px(-1.))
.border(px(1.))
.border_color(titlebar_color),
})

View File

@@ -1,6 +1,6 @@
use gpui::{Corner, Entity, Pixels, Point};
use crate::{ContextMenu, PopoverMenu, prelude::*};
use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
use super::PopoverMenuHandle;
@@ -137,36 +137,52 @@ impl RenderOnce for DropdownMenu {
let full_width = self.full_width;
let trigger_size = self.trigger_size;
let button = match self.label {
LabelKind::Text(text) => Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
LabelKind::Element(_element) => Button::new(self.id.clone(), "")
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
}
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index));
let (text_button, element_button) = match self.label {
LabelKind::Text(text) => (
Some(
Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
None,
),
LabelKind::Element(element) => (
None,
Some(
ButtonLike::new(self.id.clone())
.child(element)
.style(button_style)
.when(self.chevron, |this| {
this.child(
Icon::new(IconName::ChevronUpDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
),
};
PopoverMenu::new((self.id.clone(), "popover"))
.full_width(self.full_width)
.menu(move |_window, _cx| Some(self.menu.clone()))
.trigger(button)
.when_some(text_button, |this, text_button| this.trigger(text_button))
.when_some(element_button, |this, element_button| {
this.trigger(element_button)
})
.attach(match self.attach {
Some(attach) => attach,
None => Corner::BottomRight,

View File

@@ -15,9 +15,16 @@ impl HighlightedLabel {
/// Constructs a label with the given characters highlighted.
/// Characters are identified by UTF-8 byte position.
pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
let label = label.into();
for &run in &highlight_indices {
assert!(
label.is_char_boundary(run),
"highlight index {run} is not a valid UTF-8 boundary"
);
}
Self {
base: LabelLike::new(),
label: label.into(),
label,
highlight_indices,
}
}

View File

@@ -369,6 +369,8 @@ impl Vim {
let mut selections = Vec::new();
let mut row = tail.row();
let going_up = tail.row() > head.row();
let direction = if going_up { -1 } else { 1 };
loop {
let laid_out_line = map.layout_row(row, &text_layout_details);
@@ -399,13 +401,18 @@ impl Vim {
selections.push(selection);
}
if row == head.row() {
// When dealing with soft wrapped lines, it's possible that
// `row` ends up being set to a value other than `head.row()` as
// `head.row()` might be a `DisplayPoint` mapped to a soft
// wrapped line, hence the need for `<=` and `>=` instead of
// `==`.
if going_up && row <= head.row() || !going_up && row >= head.row() {
break;
}
// 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 };
// Find the next or previous buffer row where the `row` should
// be moved to, so that wrapped lines are skipped.
row = start_of_relative_buffer_row(map, DisplayPoint::new(row, 0), direction).row();
}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.209.0"
version = "0.209.7"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
stable

View File

@@ -49,8 +49,8 @@ You can configure Zed to format code using `eslint --fix` by running the ESLint
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
}
@@ -63,8 +63,8 @@ You can also only execute a single ESLint rule when using `fixAll`:
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
},
@@ -92,8 +92,8 @@ the formatter:
{
"languages": {
"JavaScript": {
"formatter": {
"code_action": "source.fixAll.eslint"
"code_actions_on_format": {
"source.fixAll.eslint": true
}
}
}