Compare commits

..

23 Commits

Author SHA1 Message Date
Lukas Wirth
85c2dc909d rope: Improve panic message for out of bounds anchor_at_offset (#40256)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 14:36:58 +00:00
Conrad Irwin
c814b99fcb Bump collab min version (#40198)
Release Notes:

- Prevent using Zed before the auto-update bug when collaborating.
2025-10-15 08:30:58 -06:00
localcc
07ccff217a Fix duplicate WSL entries (#40255)
Release Notes:

- N/A
2025-10-15 16:11:18 +02:00
Lukas Wirth
8ab52f3491 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-15 13:55:00 +00:00
localcc
ecf410e57d Improve musl libc detection (#40254)
Release Notes:

- N/A
2025-10-15 15:44:47 +02:00
Lukas Wirth
ec0eeaf69d rope: Assert utf8 boundary of start of Chunks::new range (#40253)
We seem to run into panics in related code, so better assert early

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 13:28:51 +00:00
Agus Zubiaga
376335496d zeta2: Numbered lines prompt format (#40218)
Adds a new `NumberedLines` format which is similar to `MarkedExcerpt`
but each line is prefixed with its line number.

Also fixes a bug where contagious snippets wouldn't get merged.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
Co-authored-by: Michael <michael@zed.dev>
2025-10-15 09:35:39 -03:00
Ben Brandt
4f656cedfa 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 12:33:17 +00:00
Ben Brandt
0e9ee3cb55 docs: Add section for configuring Codex (#40250)
Release Notes:

- N/A
2025-10-15 14:29:01 +02:00
Lukas Wirth
bbe764794d agent_servers: Honor terminal settings provided shell when fetching shell env (#40243)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 10:32:03 +00:00
Lukas Wirth
3882323f79 language: Assert CodeLabel text ranges are correct (#40242)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 10:16:56 +00:00
Lukas Wirth
b0b83ef5aa markdown_preview: Fix markdown parser producing invalid link highlights (#40239)
Fixes ZED-1YC
Fixes ZED-1YK

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 08:54:39 +00:00
Ben Brandt
7beae757b8 acp: Allow updating default mode for Codex (#40238)
Release Notes:

- acp: Save default mode for codex
2025-10-15 08:46:47 +00:00
Lukas Wirth
a6e99c1c16 project: Always use shell env in LocalLspAdapterDelegate::which (#40237)
Windows not having a default shell does not matter here, we might still
have an environment from other means (by being spawned from the cli for
example).

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 08:06:37 +00:00
Julia Ryan
6d8d2e2989 Make help docs platform specific (#40194)
No need to clutter the `--help` docs with default directories for
platforms other than the current one.

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-15 07:35:50 +00:00
Djordje
877790a105 docs: Remove duplicate Grok 4 Fast entry in models.md (#40232)
Release Notes:

- N/A
2025-10-15 07:32:01 +00:00
Conrad Irwin
0c08bbca05 Avoid gap between titlebar and body on linux (#40228)
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-15 04:44:00 +00:00
Cole Miller
ba0b68779d Fix triggers for debugger thread and session lists not rendering (#40227)
Release Notes:

- N/A
2025-10-15 04:36:04 +00:00
Cole Miller
45af5e4239 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:05:19 -04:00
Mikayla Maki
01f9b1e9b4 chore: VSCode -> VS Code (#40224)
Release Notes:

- N/A
2025-10-15 02:21:37 +00:00
Mikayla Maki
635b71c486 chore: Delete main.py (#40221)
Release Notes:

- N/A
2025-10-15 01:32:46 +00:00
Mikayla Maki
c4a7552a04 Bump Zed to v0.210 (#40219)
Release Notes:

- N/A
2025-10-15 01:10:56 +00:00
Mikayla Maki
918aee550c docs: Update releases.md (#40220)
Release Notes:

- N/A
2025-10-15 00:55:40 +00:00
81 changed files with 1380 additions and 1342 deletions

View File

@@ -882,8 +882,7 @@ jobs:
auto-release-preview:
name: Auto release preview
if: |
false
&& startsWith(github.ref, 'refs/tags/v')
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,26 +38,6 @@ 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,7 +1336,8 @@ dependencies = [
[[package]]
name = "async-tar"
version = "0.5.0"
source = "git+https://github.com/zed-industries/async-tar?rev=8af312477196311c9ea4097f2a22022f6d609bf6#8af312477196311c9ea4097f2a22022f6d609bf6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8"
dependencies = [
"async-std",
"filetime",
@@ -21202,7 +21203,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.209.6"
version = "0.210.0"
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 = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" }
async-tar = "0.5.0"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"

View File

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

View File

@@ -328,7 +328,7 @@ impl ToolCall {
location: acp::ToolCallLocation,
project: WeakEntity<Project>,
cx: &mut AsyncApp,
) -> Option<ResolvedLocation> {
) -> Option<AgentLocation> {
let buffer = project
.update(cx, |project, cx| {
project
@@ -350,14 +350,17 @@ impl ToolCall {
})
.ok()?;
Some(ResolvedLocation { buffer, position })
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
})
}
fn resolve_locations(
&self,
project: Entity<Project>,
cx: &mut App,
) -> Task<Vec<Option<ResolvedLocation>>> {
) -> Task<Vec<Option<AgentLocation>>> {
let locations = self.locations.clone();
project.update(cx, |_, cx| {
cx.spawn(async move |project, cx| {
@@ -371,23 +374,6 @@ 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
@@ -1407,46 +1393,35 @@ 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| {
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);
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 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

@@ -9,6 +9,7 @@ use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use settings::{Settings as _, SettingsLocation};
use task::Shell;
use util::{ResultExt as _, get_default_system_shell_preferring_bash};
@@ -22,7 +23,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
use terminal::TerminalBuilder;
use terminal::terminal_settings::{AlternateScroll, CursorShape};
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
#[derive(Debug, Error)]
#[error("Unsupported version")]
@@ -818,13 +819,25 @@ impl acp::Client for ClientDelegate {
let mut env = if let Some(dir) = &args.cwd {
project
.update(&mut self.cx.clone(), |project, cx| {
project.directory_environment(&task::Shell::System, dir.clone().into(), cx)
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.clone().into(), cx)
})?
.await
.unwrap_or_default()
} else {
Default::default()
};
// Disables paging for `git` and hopefully other commands
env.insert("PAGER".into(), "".into());
for var in args.env {
env.insert(var.name, var.value);
}

View File

@@ -12,7 +12,7 @@ use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
@@ -673,7 +673,7 @@ impl ContextPickerCompletionProvider {
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
@@ -682,9 +682,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
label.push_str(directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
label.build()
}
impl CompletionProvider for ContextPickerCompletionProvider {

View File

@@ -278,7 +278,7 @@ pub struct AcpThreadView {
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
collapsed_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
@@ -292,8 +292,6 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool,
}
enum ThreadState {
@@ -396,10 +394,6 @@ impl AcpThreadView {
),
];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex);
Self {
agent: agent.clone(),
workspace: workspace.clone(),
@@ -425,7 +419,7 @@ impl AcpThreadView {
thread_error: None,
thread_feedback: Default::default(),
auth_task: None,
expanded_tool_calls: HashSet::default(),
collapsed_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
editing_message: None,
edits_expanded: false,
@@ -442,8 +436,6 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning,
}
}
@@ -962,17 +954,17 @@ impl AcpThreadView {
) {
match &event.view_event {
ViewEvent::NewDiff(tool_call_id) => {
if AgentSettings::get_global(cx).expand_edit_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
if !AgentSettings::get_global(cx).expand_edit_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::NewTerminal(tool_call_id) => {
if AgentSettings::get_global(cx).expand_terminal_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
if !AgentSettings::get_global(cx).expand_terminal_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
self.expanded_tool_calls.remove(tool_call_id);
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
if let Some(thread) = self.thread()
@@ -2127,7 +2119,7 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
let tool_output_display =
if is_open {
@@ -2277,9 +2269,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
this.collapsed_tool_calls.insert(id.clone());
} else {
this.expanded_tool_calls.insert(id.clone());
this.collapsed_tool_calls.remove(&id);
}
cx.notify();
}
@@ -2481,7 +2473,7 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.on_click(cx.listener({
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
this.expanded_tool_calls.remove(&tool_call_id);
this.collapsed_tool_calls.insert(tool_call_id.clone());
cx.notify();
}
})),
@@ -2759,7 +2751,7 @@ impl AcpThreadView {
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
let header = h_flex()
.id(header_id)
@@ -2894,9 +2886,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.expanded_tool_calls.remove(&id);
this.collapsed_tool_calls.insert(id.clone());
} else {
this.expanded_tool_calls.insert(id.clone());
this.collapsed_tool_calls.remove(&id);
}
}
})),
@@ -5033,49 +5025,6 @@ impl AcpThreadView {
)
}
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning {
Some(
Callout::new()
.icon(IconName::Warning)
.severity(Severity::Warning)
.title("Codex on Windows")
.description(
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
)
.actions_slot(
Button::new("open-wsl-modal", "Open in WSL")
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx,
);
cx.notify();
}
})),
)
.dismiss_action(
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Warning"))
.on_click(cx.listener({
move |this, _, _, cx| {
this.show_codex_windows_warning = false;
cx.notify();
}
})),
),
)
} else {
None
}
}
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let content = match self.thread_error.as_ref()? {
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
@@ -5563,16 +5512,6 @@ impl Render for AcpThreadView {
_ => this,
})
.children(self.render_thread_retry_status_callout(window, cx))
.children({
#[cfg(target_os = "windows")]
{
self.render_codex_windows_warning(cx)
}
#[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
}
})
.children(self.render_thread_error(window, cx))
.when_some(
self.new_server_version_available.as_ref().filter(|_| {

View File

@@ -222,11 +222,12 @@ enum WhichFontSize {
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
NativeAgent,
Zed,
TextThread,
Gemini,
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
@@ -236,7 +237,8 @@ pub enum AgentType {
impl AgentType {
fn label(&self) -> SharedString {
match self {
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code".into(),
Self::Codex => "Codex".into(),
@@ -246,7 +248,7 @@ impl AgentType {
fn icon(&self) -> Option<IconName> {
match self {
Self::NativeAgent | Self::TextThread => None,
Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude),
Self::Codex => Some(IconName::AiOpenAi),
@@ -811,7 +813,7 @@ impl AgentPanel {
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Serialize, Deserialize)]
#[derive(Default, Serialize, Deserialize)]
struct LastUsedExternalAgent {
agent: crate::ExternalAgent,
}
@@ -852,18 +854,18 @@ impl AgentPanel {
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.map(|agent| agent.agent)
.unwrap_or(ExternalAgent::NativeAgent)
.unwrap_or_default()
.agent
}
}
};
let server = ext_agent.server(fs, history);
if !loading {
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
}
let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| {
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
@@ -1343,6 +1345,15 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
match agent {
AgentType::Zed => {
window.dispatch_action(
NewThread {
from_thread_id: None,
}
.boxed_clone(),
cx,
);
}
AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}

View File

@@ -161,9 +161,10 @@ pub struct NewNativeAgentThreadFromSummary {
}
// TODO unify this with AgentType
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExternalAgent {
enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
Codex,
@@ -183,13 +184,13 @@ fn placeholder_command() -> AgentServerCommand {
}
impl ExternalAgent {
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
match server.telemetry_id() {
"gemini-cli" => Some(Self::Gemini),
"claude-code" => Some(Self::ClaudeCode),
"codex" => Some(Self::Codex),
"zed" => Some(Self::NativeAgent),
_ => None,
fn name(&self) -> &'static str {
match self {
Self::NativeAgent => "zed",
Self::Gemini => "gemini-cli",
Self::ClaudeCode => "claude-code",
Self::Codex => "codex",
Self::Custom { .. } => "custom",
}
}

View File

@@ -11,7 +11,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::SymbolLocation;
use project::{
@@ -686,7 +686,8 @@ impl ContextPickerCompletionProvider {
};
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::plain(symbol.name.clone(), None);
let mut label = CodeLabelBuilder::default();
label.push_str(&symbol.name, None);
label.push_str(" ", None);
label.push_str(&file_name, comment_id);
label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
@@ -696,7 +697,7 @@ impl ContextPickerCompletionProvider {
Some(Completion {
replace_range: source_range.clone(),
new_text,
label,
label: label.build(),
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
@@ -729,7 +730,7 @@ impl ContextPickerCompletionProvider {
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
@@ -738,9 +739,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
label.push_str(directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
label.build()
}
impl CompletionProvider for ContextPickerCompletionProvider {

View File

@@ -84,32 +84,10 @@ 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(
@@ -131,7 +109,6 @@ impl ZedAiOnboarding {
}
}),
)
.children(self.render_dismiss_button())
.into_any_element()
}
@@ -203,7 +180,27 @@ impl ZedAiOnboarding {
)
.child(PlanDefinitions.free_plan(is_v2)),
)
.children(self.render_dismiss_button())
.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)
}),
),
)
},
)
.child(
v_flex()
.mt_2()
@@ -248,7 +245,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_trial(is_v2, false))
.children(self.render_dismiss_button())
.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)
}),
),
)
},
)
.into_any_element()
}
@@ -262,7 +278,26 @@ impl ZedAiOnboarding {
.mb_2(),
)
.child(PlanDefinitions.pro_plan(is_v2, false))
.children(self.render_dismiss_button())
.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)
}),
),
)
},
)
.into_any_element()
}
}

View File

@@ -9,6 +9,7 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::CodeLabelBuilder;
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
@@ -328,15 +329,15 @@ impl SlashCommandLine {
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(command_name, None);
label.respan_filter_range(None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
label.build()
}
#[cfg(test)]

View File

@@ -7,7 +7,7 @@ use futures::Stream;
use futures::channel::mpsc;
use fuzzy::PathMatch;
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
@@ -168,7 +168,7 @@ impl SlashCommand for FileSlashCommand {
.display(path_style)
.to_string();
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
let file_name = path_match.path.file_name()?;
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
@@ -178,10 +178,10 @@ impl SlashCommand for FileSlashCommand {
label.push_str(label_text.as_str(), None);
label.push_str(&text, comment_id);
label.filter_range = 0..file_name.len();
label.respan_filter_range(Some(file_name));
Some(ArgumentCompletion {
label,
label: label.build(),
new_text: text,
after_completion: AfterCompletion::Compose,
replace_previous_arguments: false,

View File

@@ -7,7 +7,7 @@ use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
use std::sync::{Arc, atomic::AtomicBool};
use ui::{ActiveTheme, App, Window, prelude::*};
use util::{ResultExt, paths::PathStyle};
@@ -308,10 +308,10 @@ fn create_tab_completion_label(
comment_id: Option<HighlightId>,
) -> CodeLabel {
let (parent_path, file_name) = path_style.split(path);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
label.push_str(parent_path.unwrap_or_default(), comment_id);
label.filter_range = 0..file_name.len();
label
label.respan_filter_range(Some(file_name));
label.build()
}

View File

@@ -68,10 +68,13 @@ struct Args {
#[arg(short, long, overrides_with = "add")]
new: bool,
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
/// This overrides the default platform-specific data directory location.
/// On macOS, the default is `~/Library/Application Support/Zed`.
/// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.
/// On Windows, the default is `%LOCALAPPDATA%\Zed`.
/// This overrides the default platform-specific data directory location:
#[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
#[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
#[cfg_attr(
not(any(target_os = "windows", target_os = "macos")),
doc = "`$XDG_DATA_HOME/zed`."
)]
#[arg(long, value_name = "DIR")]
user_data_dir: Option<String>,
/// The paths to open in Zed (space-separated).

View File

@@ -1,7 +1,7 @@
use chrono::Duration;
use serde::{Deserialize, Serialize};
use std::{
ops::Range,
ops::{Add, Range, Sub},
path::{Path, PathBuf},
sync::Arc,
};
@@ -18,8 +18,8 @@ pub struct PredictEditsRequest {
pub excerpt_path: Arc<Path>,
/// Within file
pub excerpt_range: Range<usize>,
/// Within `excerpt`
pub cursor_offset: usize,
pub excerpt_line_range: Range<Line>,
pub cursor_point: Point,
/// Within `signatures`
pub excerpt_parent: Option<usize>,
pub signatures: Vec<Signature>,
@@ -47,12 +47,13 @@ pub struct PredictEditsRequest {
pub enum PromptFormat {
MarkedExcerpt,
LabeledSections,
NumberedLines,
/// Prompt format intended for use via zeta_cli
OnlySnippets,
}
impl PromptFormat {
pub const DEFAULT: PromptFormat = PromptFormat::LabeledSections;
pub const DEFAULT: PromptFormat = PromptFormat::NumberedLines;
}
impl Default for PromptFormat {
@@ -73,6 +74,7 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::MarkedExcerpt => write!(f, "Marked Excerpt"),
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
PromptFormat::NumberedLines => write!(f, "Numbered Lines"),
}
}
}
@@ -97,7 +99,7 @@ pub struct Signature {
pub parent_index: Option<usize>,
/// Range of `text` within the file, possibly truncated according to `text_is_truncated`. The
/// file is implicitly the file that contains the descendant declaration or excerpt.
pub range: Range<usize>,
pub range: Range<Line>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -106,7 +108,7 @@ pub struct ReferencedDeclaration {
pub text: String,
pub text_is_truncated: bool,
/// Range of `text` within file, possibly truncated according to `text_is_truncated`
pub range: Range<usize>,
pub range: Range<Line>,
/// Range within `text`
pub signature_range: Range<usize>,
/// Index within `signatures`.
@@ -169,10 +171,36 @@ pub struct DebugInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edit {
pub path: Arc<Path>,
pub range: Range<usize>,
pub range: Range<Line>,
pub content: String,
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
pub struct Point {
pub line: Line,
pub column: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
#[serde(transparent)]
pub struct Line(pub u32);
impl Add for Line {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Line {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}

View File

@@ -1,7 +1,9 @@
//! Zeta2 prompt planning and generation code shared with cloud.
use anyhow::{Context as _, Result, anyhow};
use cloud_llm_client::predict_edits_v3::{self, Event, PromptFormat, ReferencedDeclaration};
use cloud_llm_client::predict_edits_v3::{
self, Event, Line, Point, PromptFormat, ReferencedDeclaration,
};
use indoc::indoc;
use ordered_float::OrderedFloat;
use rustc_hash::{FxHashMap, FxHashSet};
@@ -43,6 +45,42 @@ const LABELED_SECTIONS_SYSTEM_PROMPT: &str = indoc! {r#"
}
"#};
const NUMBERED_LINES_SYSTEM_PROMPT: &str = indoc! {r#"
# Instructions
You are a code completion assistant helping a programmer finish their work. Your task is to:
1. Analyze the edit history to understand what the programmer is trying to achieve
2. Identify any incomplete refactoring or changes that need to be finished
3. Make the remaining edits that a human programmer would logically make next
4. Apply systematic changes consistently across the entire codebase - if you see a pattern starting, complete it everywhere.
Focus on:
- Understanding the intent behind the changes (e.g., improving error handling, refactoring APIs, fixing bugs)
- Completing any partially-applied changes across the codebase
- Ensuring consistency with the programming style and patterns already established
- Making edits that maintain or improve code quality
- If the programmer started refactoring one instance of a pattern, find and update ALL similar instances
- Don't write a lot of code if you're not sure what to do
Rules:
- Do not just mechanically apply patterns - reason about what changes make sense given the context and the programmer's apparent goals.
- Do not just fix syntax errors - look for the broader refactoring pattern and apply it systematically throughout the code.
- Write the edits in the unified diff format as shown in the example.
# Example output:
```
--- a/distill-claude/tmp-outs/edits_history.txt
+++ b/distill-claude/tmp-outs/edits_history.txt
@@ -1,3 +1,3 @@
-
-
-import sys
+import json
```
"#};
pub struct PlannedPrompt<'a> {
request: &'a predict_edits_v3::PredictEditsRequest,
/// Snippets to include in the prompt. These may overlap - they are merged / deduplicated in
@@ -55,6 +93,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
match format {
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_SYSTEM_PROMPT,
PromptFormat::LabeledSections => LABELED_SECTIONS_SYSTEM_PROMPT,
PromptFormat::NumberedLines => NUMBERED_LINES_SYSTEM_PROMPT,
// only intended for use via zeta_cli
PromptFormat::OnlySnippets => "",
}
@@ -63,7 +102,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
#[derive(Clone, Debug)]
pub struct PlannedSnippet<'a> {
path: Arc<Path>,
range: Range<usize>,
range: Range<Line>,
text: &'a str,
// TODO: Indicate this in the output
#[allow(dead_code)]
@@ -79,7 +118,7 @@ pub enum DeclarationStyle {
#[derive(Clone, Debug, Serialize)]
pub struct SectionLabels {
pub excerpt_index: usize,
pub section_ranges: Vec<(Arc<Path>, Range<usize>)>,
pub section_ranges: Vec<(Arc<Path>, Range<Line>)>,
}
impl<'a> PlannedPrompt<'a> {
@@ -196,10 +235,24 @@ impl<'a> PlannedPrompt<'a> {
declaration.text.len()
));
};
let signature_start_line = declaration.range.start
+ Line(
declaration.text[..declaration.signature_range.start]
.lines()
.count() as u32,
);
let signature_end_line = signature_start_line
+ Line(
declaration.text
[declaration.signature_range.start..declaration.signature_range.end]
.lines()
.count() as u32,
);
let range = signature_start_line..signature_end_line;
PlannedSnippet {
path: declaration.path.clone(),
range: (declaration.signature_range.start + declaration.range.start)
..(declaration.signature_range.end + declaration.range.start),
range,
text,
text_is_truncated: declaration.text_is_truncated,
}
@@ -318,7 +371,7 @@ impl<'a> PlannedPrompt<'a> {
}
let excerpt_snippet = PlannedSnippet {
path: self.request.excerpt_path.clone(),
range: self.request.excerpt_range.clone(),
range: self.request.excerpt_line_range.clone(),
text: &self.request.excerpt,
text_is_truncated: false,
};
@@ -328,32 +381,33 @@ impl<'a> PlannedPrompt<'a> {
let mut excerpt_file_insertions = match self.request.prompt_format {
PromptFormat::MarkedExcerpt => vec![
(
self.request.excerpt_range.start,
Point {
line: self.request.excerpt_line_range.start,
column: 0,
},
EDITABLE_REGION_START_MARKER_WITH_NEWLINE,
),
(self.request.cursor_point, CURSOR_MARKER),
(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
),
(
self.request
.excerpt_range
.end
.saturating_sub(0)
.max(self.request.excerpt_range.start),
Point {
line: self.request.excerpt_line_range.end,
column: 0,
},
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
],
PromptFormat::LabeledSections => vec![(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
)],
PromptFormat::LabeledSections => vec![(self.request.cursor_point, CURSOR_MARKER)],
PromptFormat::NumberedLines => vec![(self.request.cursor_point, CURSOR_MARKER)],
PromptFormat::OnlySnippets => vec![],
};
let mut prompt = String::new();
prompt.push_str("## User Edits\n\n");
Self::push_events(&mut prompt, &self.request.events);
if self.request.events.is_empty() {
prompt.push_str("No edits yet.\n");
} else {
Self::push_events(&mut prompt, &self.request.events);
}
prompt.push_str("\n## Code\n\n");
let section_labels =
@@ -391,13 +445,17 @@ impl<'a> PlannedPrompt<'a> {
if *predicted {
writeln!(
output,
"User accepted prediction {:?}:\n```diff\n{}\n```\n",
"User accepted prediction {:?}:\n`````diff\n{}\n`````\n",
path, diff
)
.unwrap();
} else {
writeln!(output, "User edited {:?}:\n```diff\n{}\n```\n", path, diff)
.unwrap();
writeln!(
output,
"User edited {:?}:\n`````diff\n{}\n`````\n",
path, diff
)
.unwrap();
}
}
}
@@ -407,7 +465,7 @@ impl<'a> PlannedPrompt<'a> {
fn push_file_snippets(
&self,
output: &mut String,
excerpt_file_insertions: &mut Vec<(usize, &'static str)>,
excerpt_file_insertions: &mut Vec<(Point, &'static str)>,
file_snippets: Vec<(&'a Path, Vec<&'a PlannedSnippet>, bool)>,
) -> Result<SectionLabels> {
let mut section_ranges = Vec::new();
@@ -417,15 +475,13 @@ impl<'a> PlannedPrompt<'a> {
snippets.sort_by_key(|s| (s.range.start, Reverse(s.range.end)));
// TODO: What if the snippets get expanded too large to be editable?
let mut current_snippet: Option<(&PlannedSnippet, Range<usize>)> = None;
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<usize>)> = Vec::new();
let mut current_snippet: Option<(&PlannedSnippet, Range<Line>)> = None;
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<Line>)> = Vec::new();
for snippet in snippets {
if let Some((_, current_snippet_range)) = current_snippet.as_mut()
&& snippet.range.start < current_snippet_range.end
&& snippet.range.start <= current_snippet_range.end
{
if snippet.range.end > current_snippet_range.end {
current_snippet_range.end = snippet.range.end;
}
current_snippet_range.end = current_snippet_range.end.max(snippet.range.end);
continue;
}
if let Some(current_snippet) = current_snippet.take() {
@@ -437,21 +493,24 @@ impl<'a> PlannedPrompt<'a> {
disjoint_snippets.push(current_snippet);
}
writeln!(output, "```{}", file_path.display()).ok();
// TODO: remove filename=?
writeln!(output, "`````filename={}", file_path.display()).ok();
let mut skipped_last_snippet = false;
for (snippet, range) in disjoint_snippets {
let section_index = section_ranges.len();
match self.request.prompt_format {
PromptFormat::MarkedExcerpt | PromptFormat::OnlySnippets => {
if range.start > 0 && !skipped_last_snippet {
PromptFormat::MarkedExcerpt
| PromptFormat::OnlySnippets
| PromptFormat::NumberedLines => {
if range.start.0 > 0 && !skipped_last_snippet {
output.push_str("\n");
}
}
PromptFormat::LabeledSections => {
if is_excerpt_file
&& range.start <= self.request.excerpt_range.start
&& range.end >= self.request.excerpt_range.end
&& range.start <= self.request.excerpt_line_range.start
&& range.end >= self.request.excerpt_line_range.end
{
writeln!(output, "<|current_section|>").ok();
} else {
@@ -460,46 +519,83 @@ impl<'a> PlannedPrompt<'a> {
}
}
let push_full_snippet = |output: &mut String| {
if self.request.prompt_format == PromptFormat::NumberedLines {
for (i, line) in snippet.text.lines().enumerate() {
writeln!(output, "{}|{}", i as u32 + range.start.0 + 1, line)?;
}
} else {
output.push_str(&snippet.text);
}
anyhow::Ok(())
};
if is_excerpt_file {
if self.request.prompt_format == PromptFormat::OnlySnippets {
if range.start >= self.request.excerpt_range.start
&& range.end <= self.request.excerpt_range.end
if range.start >= self.request.excerpt_line_range.start
&& range.end <= self.request.excerpt_line_range.end
{
skipped_last_snippet = true;
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
}
} else {
let mut last_offset = range.start;
let mut i = 0;
while i < excerpt_file_insertions.len() {
let (offset, insertion) = &excerpt_file_insertions[i];
let found = *offset >= range.start && *offset <= range.end;
} else if !excerpt_file_insertions.is_empty() {
let lines = snippet.text.lines().collect::<Vec<_>>();
let push_line = |output: &mut String, line_ix: usize| {
if self.request.prompt_format == PromptFormat::NumberedLines {
write!(output, "{}|", line_ix as u32 + range.start.0 + 1)?;
}
anyhow::Ok(writeln!(output, "{}", lines[line_ix])?)
};
let mut last_line_ix = 0;
let mut insertion_ix = 0;
while insertion_ix < excerpt_file_insertions.len() {
let (point, insertion) = &excerpt_file_insertions[insertion_ix];
let found = point.line >= range.start && point.line <= range.end;
if found {
excerpt_index = Some(section_index);
output.push_str(
&snippet.text[last_offset - range.start..offset - range.start],
);
output.push_str(insertion);
last_offset = *offset;
excerpt_file_insertions.remove(i);
let insertion_line_ix = (point.line.0 - range.start.0) as usize;
for line_ix in last_line_ix..insertion_line_ix {
push_line(output, line_ix)?;
}
if let Some(next_line) = lines.get(insertion_line_ix) {
if self.request.prompt_format == PromptFormat::NumberedLines {
write!(
output,
"{}|",
insertion_line_ix as u32 + range.start.0 + 1
)?
}
output.push_str(&next_line[..point.column as usize]);
output.push_str(insertion);
writeln!(output, "{}", &next_line[point.column as usize..])?;
} else {
writeln!(output, "{}", insertion)?;
}
last_line_ix = insertion_line_ix + 1;
excerpt_file_insertions.remove(insertion_ix);
continue;
}
i += 1;
insertion_ix += 1;
}
skipped_last_snippet = false;
output.push_str(&snippet.text[last_offset - range.start..]);
for line_ix in last_line_ix..lines.len() {
push_line(output, line_ix)?;
}
} else {
skipped_last_snippet = false;
push_full_snippet(output)?;
}
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
push_full_snippet(output)?;
}
section_ranges.push((snippet.path.clone(), range));
}
output.push_str("```\n\n");
output.push_str("`````\n\n");
}
Ok(SectionLabels {

View File

@@ -30,9 +30,9 @@ impl fmt::Display for ZedVersion {
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
// v0.198.4 is the first version where we no longer connect to Collab automatically.
// We reject any clients older than that to prevent them from connecting to Collab just for authentication.
if self.0 < SemanticVersion::new(0, 198, 4) {
// v0.204.1 was the first version after the auto-update bug.
// We reject any clients older than that to hope we can persuade them to upgrade.
if self.0 < SemanticVersion::new(0, 204, 1) {
return false;
}

View File

@@ -669,11 +669,7 @@ impl ConsoleQueryBarCompletionProvider {
&snapshot,
),
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: string_match.string.clone(),
runs: Vec::new(),
},
label: CodeLabel::plain(string_match.string.clone(), None),
icon_path: None,
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
variable_value.into(),
@@ -782,11 +778,7 @@ impl ConsoleQueryBarCompletionProvider {
&snapshot,
),
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label,
runs: Vec::new(),
},
label: CodeLabel::plain(completion.label, None),
icon_path: None,
documentation: completion.detail.map(|detail| {
CompletionDocumentation::MultiLineMarkdown(detail.into())

View File

@@ -1,3 +1,4 @@
use cloud_llm_client::predict_edits_v3::{self, Line};
use language::{Language, LanguageId};
use project::ProjectEntryId;
use std::ops::Range;
@@ -91,6 +92,18 @@ impl Declaration {
}
}
pub fn item_line_range(&self) -> Range<Line> {
match self {
Declaration::File { declaration, .. } => declaration.item_line_range.clone(),
Declaration::Buffer {
declaration, rope, ..
} => {
Line(rope.offset_to_point(declaration.item_range.start).row)
..Line(rope.offset_to_point(declaration.item_range.end).row)
}
}
}
pub fn item_text(&self) -> (Cow<'_, str>, bool) {
match self {
Declaration::File { declaration, .. } => (
@@ -130,6 +143,18 @@ impl Declaration {
}
}
pub fn signature_line_range(&self) -> Range<Line> {
match self {
Declaration::File { declaration, .. } => declaration.signature_line_range.clone(),
Declaration::Buffer {
declaration, rope, ..
} => {
Line(rope.offset_to_point(declaration.signature_range.start).row)
..Line(rope.offset_to_point(declaration.signature_range.end).row)
}
}
}
pub fn signature_range_in_item_text(&self) -> Range<usize> {
let signature_range = self.signature_range();
let item_range = self.item_range();
@@ -142,7 +167,7 @@ fn expand_range_to_line_boundaries_and_truncate(
range: &Range<usize>,
limit: usize,
rope: &Rope,
) -> (Range<usize>, bool) {
) -> (Range<usize>, Range<predict_edits_v3::Line>, bool) {
let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
point_range.start.column = 0;
point_range.end.row += 1;
@@ -155,7 +180,10 @@ fn expand_range_to_line_boundaries_and_truncate(
item_range.end = item_range.start + limit;
}
item_range.end = rope.clip_offset(item_range.end, Bias::Left);
(item_range, is_truncated)
let line_range =
predict_edits_v3::Line(point_range.start.row)..predict_edits_v3::Line(point_range.end.row);
(item_range, line_range, is_truncated)
}
#[derive(Debug, Clone)]
@@ -164,25 +192,30 @@ pub struct FileDeclaration {
pub identifier: Identifier,
/// offset range of the declaration in the file, expanded to line boundaries and truncated
pub item_range: Range<usize>,
/// line range of the declaration in the file, potentially truncated
pub item_line_range: Range<predict_edits_v3::Line>,
/// text of `item_range`
pub text: Arc<str>,
/// whether `text` was truncated
pub text_is_truncated: bool,
/// offset range of the signature in the file, expanded to line boundaries and truncated
pub signature_range: Range<usize>,
/// line range of the signature in the file, truncated
pub signature_line_range: Range<Line>,
/// whether `signature` was truncated
pub signature_is_truncated: bool,
}
impl FileDeclaration {
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
let (item_range_in_file, text_is_truncated) = expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (item_range_in_file, item_line_range_in_file, text_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (mut signature_range_in_file, mut signature_is_truncated) =
let (mut signature_range_in_file, signature_line_range, mut signature_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.signature_range,
ITEM_TEXT_TRUNCATION_LENGTH,
@@ -202,6 +235,7 @@ impl FileDeclaration {
parent: None,
identifier: declaration.identifier,
signature_range: signature_range_in_file,
signature_line_range,
signature_is_truncated,
text: rope
.chunks_in_range(item_range_in_file.clone())
@@ -209,6 +243,7 @@ impl FileDeclaration {
.into(),
text_is_truncated,
item_range: item_range_in_file,
item_line_range: item_line_range_in_file,
}
}
}
@@ -225,12 +260,13 @@ pub struct BufferDeclaration {
impl BufferDeclaration {
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
let (item_range, item_range_is_truncated) = expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (signature_range, signature_range_is_truncated) =
let (item_range, _item_line_range, item_range_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (signature_range, _signature_line_range, signature_range_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.signature_range,
ITEM_TEXT_TRUNCATION_LENGTH,

View File

@@ -9,6 +9,7 @@ pub mod text_similarity;
use std::{path::Path, sync::Arc};
use cloud_llm_client::predict_edits_v3;
use collections::HashMap;
use gpui::{App, AppContext as _, Entity, Task};
use language::BufferSnapshot;
@@ -21,6 +22,8 @@ pub use imports::*;
pub use reference::*;
pub use syntax_index::*;
pub use predict_edits_v3::Line;
#[derive(Clone, Debug, PartialEq)]
pub struct EditPredictionContextOptions {
pub use_imports: bool,
@@ -32,7 +35,7 @@ pub struct EditPredictionContextOptions {
pub struct EditPredictionContext {
pub excerpt: EditPredictionExcerpt,
pub excerpt_text: EditPredictionExcerptText,
pub cursor_offset_in_excerpt: usize,
pub cursor_point: Point,
pub declarations: Vec<ScoredDeclaration>,
}
@@ -124,8 +127,6 @@ impl EditPredictionContext {
);
let cursor_offset_in_file = cursor_point.to_offset(buffer);
// TODO fix this to not need saturating_sub
let cursor_offset_in_excerpt = cursor_offset_in_file.saturating_sub(excerpt.range.start);
let declarations = if let Some(index_state) = index_state {
let references = get_references(&excerpt, &excerpt_text, buffer);
@@ -148,7 +149,7 @@ impl EditPredictionContext {
Some(Self {
excerpt,
excerpt_text,
cursor_offset_in_excerpt,
cursor_point,
declarations,
})
}

View File

@@ -4,7 +4,7 @@ use text::{Point, ToOffset as _, ToPoint as _};
use tree_sitter::{Node, TreeCursor};
use util::RangeExt;
use crate::{BufferDeclaration, declaration::DeclarationId, syntax_index::SyntaxIndexState};
use crate::{BufferDeclaration, Line, declaration::DeclarationId, syntax_index::SyntaxIndexState};
// TODO:
//
@@ -35,6 +35,7 @@ pub struct EditPredictionExcerptOptions {
#[derive(Debug, Clone)]
pub struct EditPredictionExcerpt {
pub range: Range<usize>,
pub line_range: Range<Line>,
pub parent_declarations: Vec<(DeclarationId, Range<usize>)>,
pub size: usize,
}
@@ -86,12 +87,19 @@ impl EditPredictionExcerpt {
buffer.len(),
options.max_bytes
);
return Some(EditPredictionExcerpt::new(0..buffer.len(), Vec::new()));
let offset_range = 0..buffer.len();
let line_range = Line(0)..Line(buffer.max_point().row);
return Some(EditPredictionExcerpt::new(
offset_range,
line_range,
Vec::new(),
));
}
let query_offset = query_point.to_offset(buffer);
let query_range = Point::new(query_point.row, 0).to_offset(buffer)
..Point::new(query_point.row + 1, 0).to_offset(buffer);
let query_line_range = query_point.row..query_point.row + 1;
let query_range = Point::new(query_line_range.start, 0).to_offset(buffer)
..Point::new(query_line_range.end, 0).to_offset(buffer);
if query_range.len() >= options.max_bytes {
return None;
}
@@ -107,6 +115,7 @@ impl EditPredictionExcerpt {
let excerpt_selector = ExcerptSelector {
query_offset,
query_range,
query_line_range: Line(query_line_range.start)..Line(query_line_range.end),
parent_declarations: &parent_declarations,
buffer,
options,
@@ -130,7 +139,11 @@ impl EditPredictionExcerpt {
excerpt_selector.select_lines()
}
fn new(range: Range<usize>, parent_declarations: Vec<(DeclarationId, Range<usize>)>) -> Self {
fn new(
range: Range<usize>,
line_range: Range<Line>,
parent_declarations: Vec<(DeclarationId, Range<usize>)>,
) -> Self {
let size = range.len()
+ parent_declarations
.iter()
@@ -140,10 +153,11 @@ impl EditPredictionExcerpt {
range,
parent_declarations,
size,
line_range,
}
}
fn with_expanded_range(&self, new_range: Range<usize>) -> Self {
fn with_expanded_range(&self, new_range: Range<usize>, new_line_range: Range<Line>) -> Self {
if !new_range.contains_inclusive(&self.range) {
// this is an issue because parent_signature_ranges may be incorrect
log::error!("bug: with_expanded_range called with disjoint range");
@@ -155,7 +169,7 @@ impl EditPredictionExcerpt {
}
parent_declarations.push((*declaration_id, range.clone()));
}
Self::new(new_range, parent_declarations)
Self::new(new_range, new_line_range, parent_declarations)
}
fn parent_signatures_size(&self) -> usize {
@@ -166,6 +180,7 @@ impl EditPredictionExcerpt {
struct ExcerptSelector<'a> {
query_offset: usize,
query_range: Range<usize>,
query_line_range: Range<Line>,
parent_declarations: &'a [(DeclarationId, &'a BufferDeclaration)],
buffer: &'a BufferSnapshot,
options: &'a EditPredictionExcerptOptions,
@@ -178,10 +193,13 @@ impl<'a> ExcerptSelector<'a> {
let mut cursor = selected_layer_root.walk();
loop {
let excerpt_range = node_line_start(cursor.node()).to_offset(&self.buffer)
..node_line_end(cursor.node()).to_offset(&self.buffer);
let line_start = node_line_start(cursor.node());
let line_end = node_line_end(cursor.node());
let line_range = Line(line_start.row)..Line(line_end.row);
let excerpt_range =
line_start.to_offset(&self.buffer)..line_end.to_offset(&self.buffer);
if excerpt_range.contains_inclusive(&self.query_range) {
let excerpt = self.make_excerpt(excerpt_range);
let excerpt = self.make_excerpt(excerpt_range, line_range);
if excerpt.size <= self.options.max_bytes {
return Some(self.expand_to_siblings(&mut cursor, excerpt));
}
@@ -272,9 +290,13 @@ impl<'a> ExcerptSelector<'a> {
let mut forward = None;
while !forward_done {
let new_end = node_line_end(forward_cursor.node()).to_offset(&self.buffer);
let new_end_point = node_line_end(forward_cursor.node());
let new_end = new_end_point.to_offset(&self.buffer);
if new_end > excerpt.range.end {
let new_excerpt = excerpt.with_expanded_range(excerpt.range.start..new_end);
let new_excerpt = excerpt.with_expanded_range(
excerpt.range.start..new_end,
excerpt.line_range.start..Line(new_end_point.row),
);
if new_excerpt.size <= self.options.max_bytes {
forward = Some(new_excerpt);
break;
@@ -289,9 +311,13 @@ impl<'a> ExcerptSelector<'a> {
let mut backward = None;
while !backward_done {
let new_start = node_line_start(backward_cursor.node()).to_offset(&self.buffer);
let new_start_point = node_line_start(backward_cursor.node());
let new_start = new_start_point.to_offset(&self.buffer);
if new_start < excerpt.range.start {
let new_excerpt = excerpt.with_expanded_range(new_start..excerpt.range.end);
let new_excerpt = excerpt.with_expanded_range(
new_start..excerpt.range.end,
Line(new_start_point.row)..excerpt.line_range.end,
);
if new_excerpt.size <= self.options.max_bytes {
backward = Some(new_excerpt);
break;
@@ -339,7 +365,7 @@ impl<'a> ExcerptSelector<'a> {
fn select_lines(&self) -> Option<EditPredictionExcerpt> {
// early return if line containing query_offset is already too large
let excerpt = self.make_excerpt(self.query_range.clone());
let excerpt = self.make_excerpt(self.query_range.clone(), self.query_line_range.clone());
if excerpt.size > self.options.max_bytes {
log::debug!(
"excerpt for cursor line is {} bytes, which exceeds the window",
@@ -353,24 +379,24 @@ impl<'a> ExcerptSelector<'a> {
let before_bytes =
(self.options.target_before_cursor_over_total_bytes * bytes_remaining as f32) as usize;
let start_point = {
let start_line = {
let offset = self.query_offset.saturating_sub(before_bytes);
let point = offset.to_point(self.buffer);
Point::new(point.row + 1, 0)
Line(point.row + 1)
};
let start_offset = start_point.to_offset(&self.buffer);
let end_point = {
let start_offset = Point::new(start_line.0, 0).to_offset(&self.buffer);
let end_line = {
let offset = start_offset + bytes_remaining;
let point = offset.to_point(self.buffer);
Point::new(point.row, 0)
Line(point.row)
};
let end_offset = end_point.to_offset(&self.buffer);
let end_offset = Point::new(end_line.0, 0).to_offset(&self.buffer);
// this could be expanded further since recalculated `signature_size` may be smaller, but
// skipping that for now for simplicity
//
// TODO: could also consider checking if lines immediately before / after fit.
let excerpt = self.make_excerpt(start_offset..end_offset);
let excerpt = self.make_excerpt(start_offset..end_offset, start_line..end_line);
if excerpt.size > self.options.max_bytes {
log::error!(
"bug: line-based excerpt selection has size {}, \
@@ -382,14 +408,14 @@ impl<'a> ExcerptSelector<'a> {
return Some(excerpt);
}
fn make_excerpt(&self, range: Range<usize>) -> EditPredictionExcerpt {
fn make_excerpt(&self, range: Range<usize>, line_range: Range<Line>) -> EditPredictionExcerpt {
let parent_declarations = self
.parent_declarations
.iter()
.filter(|(_, declaration)| declaration.item_range.contains_inclusive(&range))
.map(|(id, declaration)| (*id, declaration.signature_range.clone()))
.collect();
EditPredictionExcerpt::new(range, parent_declarations)
EditPredictionExcerpt::new(range, line_range, parent_declarations)
}
/// Returns `true` if the `forward` excerpt is a better choice than the `backward` excerpt.

View File

@@ -328,11 +328,7 @@ impl CompletionsMenu {
.map(|choice| Completion {
replace_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),
runs: Default::default(),
filter_range: Default::default(),
},
label: CodeLabel::plain(choice.to_string(), None),
icon_path: None,
documentation: None,
confirm: None,

View File

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

View File

@@ -3899,9 +3899,6 @@ impl Editor {
}
})
.collect::<Vec<_>>();
if selection_ranges.is_empty() {
return;
}
let ranges = match columnar_state {
ColumnarSelectionState::FromMouse { .. } => {
@@ -5291,8 +5288,8 @@ impl Editor {
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, cx| {
display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
self.display_map.update(cx, |display_map, _| {
display_map.remove_inlays_for_excerpts(&excerpts_removed)
});
return;
}
@@ -23080,11 +23077,7 @@ fn snippet_completions(
}),
lsp_defaults: None,
},
label: CodeLabel {
text: matching_prefix.clone(),
runs: Vec::new(),
filter_range: 0..matching_prefix.len(),
},
label: CodeLabel::plain(matching_prefix.clone(), None),
icon_path: None,
documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: snippet.name.clone().into(),

View File

@@ -14878,12 +14878,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
} else {
item.label.clone()
};
let len = text.len();
Some(language::CodeLabel {
text,
runs: Vec::new(),
filter_range: 0..len,
})
Some(language::CodeLabel::plain(text, None))
})),
..FakeLspAdapter::default()
},

View File

@@ -651,6 +651,7 @@ 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,
@@ -666,20 +667,7 @@ impl EditorElement {
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
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()),
})
{
if let Some(hovered_hunk) = hovered_hunk {
editor.toggle_single_diff_hunk(hovered_hunk, cx);
cx.notify();
return;
@@ -7259,6 +7247,26 @@ 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| {
@@ -7275,6 +7283,7 @@ impl EditorElement {
Self::mouse_left_down(
editor,
event,
diff_hunk_range.clone(),
&position_map,
line_numbers.as_ref(),
window,

View File

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

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 mut match_positions = match &self.directory_state {
let 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,38 +710,29 @@ impl PickerDelegate for OpenPathDelegate {
});
match &self.directory_state {
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),
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
},
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 {
match_positions.iter_mut().for_each(|position| {
*position += self.prompt_root.len();
});
let (label, delta) = if parent_path == &self.prompt_root {
(
format!("{}{}", self.prompt_root, candidate.path.string),
self.prompt_root.len(),
@@ -749,10 +740,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 {
@@ -764,7 +755,7 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Conflict.color(cx)),
)],
)
@@ -774,17 +765,27 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Created.color(cx)),
)],
)
.into_any_element()
}
} else {
HighlightedLabel::new(label, match_positions).into_any_element()
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(),
None => {
let mut highlight_positions = match_positions;
highlight_positions.iter_mut().for_each(|position| {
*position += delta;
});
HighlightedLabel::new(label, highlight_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, FontStyle, FontWeight, StyledText, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
size,
};
struct HelloWorld {}
@@ -71,12 +71,6 @@ 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

@@ -180,8 +180,7 @@ impl StyledText {
"Can't use `with_default_highlights` and `with_highlights`"
);
let runs = Self::compute_runs(&self.text, default_style, highlights);
self.runs = Some(runs);
self
self.with_runs(runs)
}
/// Set the styling attributes for the given text, as well as
@@ -194,7 +193,15 @@ impl StyledText {
self.runs.is_none(),
"Can't use `with_highlights` and `with_default_highlights`"
);
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
self.delayed_highlights = Some(
highlights
.into_iter()
.inspect(|(run, _)| {
debug_assert!(self.text.is_char_boundary(run.start));
debug_assert!(self.text.is_char_boundary(run.end));
})
.collect::<Vec<_>>(),
);
self
}
@@ -207,8 +214,10 @@ impl StyledText {
let mut ix = 0;
for (range, highlight) in highlights {
if ix < range.start {
debug_assert!(text.is_char_boundary(range.start));
runs.push(default_style.clone().to_run(range.start - ix));
}
debug_assert!(text.is_char_boundary(range.end));
runs.push(
default_style
.clone()
@@ -225,6 +234,11 @@ impl StyledText {
/// Set the text runs for this piece of text.
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
let mut text = &**self.text;
for run in &runs {
text = text.get(run.len..).expect("invalid text run");
}
assert!(text.is_empty(), "invalid text run");
self.runs = Some(runs);
self
}

View File

@@ -449,12 +449,11 @@ 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(); // from previous loop
let utf16_start = string.char_len(); // insert at end of string
let n_zwnjs = self.zwnjs_scratch_space.len();
let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16;
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
// 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));
string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0));
if needs_zwnj {
let zwnjs_pos = string.char_len();
self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize));
@@ -463,9 +462,10 @@ impl MacTextSystemState {
CFRange::init(zwnjs_pos, 0),
);
}
let utf16_end = string.char_len();
let utf16_end = string.char_len() as usize;
let cf_range = CFRange::init(utf16_start, utf16_end - utf16_start);
let cf_range =
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
let font = &self.fonts[run.font_id.0];
let font_metrics = font.metrics();
@@ -548,12 +548,10 @@ impl MacTextSystemState {
}
}
#[derive(Debug, Clone)]
#[derive(Clone)]
struct StringIndexConverter<'a> {
text: &'a str,
/// Index in UTF-8 bytes
utf8_ix: usize,
/// Index in UTF-16 code units
utf16_ix: usize,
}
@@ -734,25 +732,6 @@ 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,18 +530,8 @@ impl WindowsWindowInner {
};
let scale_factor = lock.scale_factor;
let wheel_scroll_amount = match modifiers.shift {
true => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars
}
false => {
self.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_lines
}
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
};
drop(lock);
@@ -584,11 +574,7 @@ impl WindowsWindowInner {
return Some(1);
};
let scale_factor = lock.scale_factor;
let wheel_scroll_chars = self
.system_settings
.borrow()
.mouse_wheel_settings
.wheel_scroll_chars;
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
drop(lock);
let wheel_distance =
@@ -721,8 +707,11 @@ impl WindowsWindowInner {
// used by Chrome. However, it may result in one row of pixels being obscured
// in our client area. But as Chrome says, "there seems to be no better solution."
if is_maximized
&& let Some(ref taskbar_position) =
self.system_settings.borrow().auto_hide_taskbar_position
&& let Some(ref taskbar_position) = self
.state
.borrow()
.system_settings
.auto_hide_taskbar_position
{
// For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
// so the window isn't treated as a "fullscreen app", which would cause
@@ -1112,11 +1101,9 @@ impl WindowsWindowInner {
if wparam.0 != 0 {
let mut lock = self.state.borrow_mut();
let display = lock.display;
lock.system_settings.update(display, wparam.0);
lock.click_state.system_update(wparam.0);
lock.border_offset.update(handle).log_err();
// system settings may emit a window message which wants to take the refcell lock, so drop it
drop(lock);
self.system_settings.borrow_mut().update(display, wparam.0);
} else {
self.handle_system_theme_changed(handle, lparam)?;
};

View File

@@ -51,6 +51,7 @@ pub struct WindowsWindowState {
pub renderer: DirectXRenderer,
pub click_state: ClickState,
pub system_settings: WindowsSystemSettings,
pub current_cursor: Option<HCURSOR>,
pub nc_button_pressed: Option<u32>,
@@ -65,7 +66,6 @@ pub(crate) struct WindowsWindowInner {
pub(super) this: Weak<Self>,
drop_target_helper: IDropTargetHelper,
pub(crate) state: RefCell<WindowsWindowState>,
pub(crate) system_settings: RefCell<WindowsSystemSettings>,
pub(crate) handle: AnyWindowHandle,
pub(crate) hide_title_bar: bool,
pub(crate) is_movable: bool,
@@ -115,6 +115,7 @@ impl WindowsWindowState {
let system_key_handled = false;
let hovered = false;
let click_state = ClickState::new();
let system_settings = WindowsSystemSettings::new(display);
let nc_button_pressed = None;
let fullscreen = None;
let initial_placement = None;
@@ -137,6 +138,7 @@ impl WindowsWindowState {
hovered,
renderer,
click_state,
system_settings,
current_cursor,
nc_button_pressed,
display,
@@ -229,7 +231,6 @@ impl WindowsWindowInner {
validation_number: context.validation_number,
main_receiver: context.main_receiver.clone(),
platform_window_handle: context.platform_window_handle,
system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
}))
}
@@ -643,12 +644,10 @@ impl PlatformWindow for WindowsWindow {
let mut btn_encoded = Vec::new();
for (index, btn) in answers.iter().enumerate() {
let encoded = HSTRING::from(btn.label().as_ref());
let button_id = match btn {
PromptButton::Ok(_) => IDOK.0,
PromptButton::Cancel(_) => IDCANCEL.0,
// the first few low integer values are reserved for known buttons
// so for simplicity we just go backwards from -1
PromptButton::Other(_) => -(index as i32) - 1,
let button_id = if btn.is_cancel() {
IDCANCEL.0
} else {
index as i32 - 100
};
button_id_map.push(button_id);
buttons.push(TASKDIALOG_BUTTON {
@@ -666,11 +665,11 @@ impl PlatformWindow for WindowsWindow {
.context("unable to create task dialog")
.log_err();
if let Some(clicked) =
button_id_map.iter().position(|&button_id| button_id == res)
{
let _ = done_tx.send(clicked);
}
let clicked = button_id_map
.iter()
.position(|&button_id| button_id == res)
.unwrap();
let _ = done_tx.send(clicked);
}
})
.detach();

View File

@@ -424,6 +424,7 @@ 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 {
@@ -452,13 +453,14 @@ impl WindowTextSystem {
true
};
let font_id = self.resolve_font(&run.font);
if let Some(font_run) = font_runs.last_mut()
&& font_id == font_run.font_id
&& Some(font_run.font_id) == last_font
&& !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

@@ -225,19 +225,15 @@ impl LineWrapper {
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
let mut truncate_at = result.len() - ellipsis.len();
let mut run_end = None;
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
run_end = Some(run_index + 1);
runs.truncate(run_index + 1);
break;
}
}
if let Some(run_end) = run_end {
runs.truncate(run_end);
}
}
/// A fragment of a line that can be wrapped.

View File

@@ -670,6 +670,16 @@ pub struct CodeLabel {
pub filter_range: Range<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodeLabelBuilder {
/// The text to display.
text: String,
/// Syntax highlighting runs.
runs: Vec<(Range<usize>, HighlightId)>,
/// The portion of the text that should be used in fuzzy filtering.
filter_range: Range<usize>,
}
#[derive(Clone, Deserialize, JsonSchema)]
pub struct LanguageConfig {
/// Human-readable name of the language.
@@ -2223,6 +2233,34 @@ impl Grammar {
}
}
impl CodeLabelBuilder {
pub fn respan_filter_range(&mut self, filter_text: Option<&str>) {
self.filter_range = filter_text
.and_then(|filter| self.text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..self.text.len());
}
pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
let start_ix = self.text.len();
self.text.push_str(text);
if let Some(highlight) = highlight {
let end_ix = self.text.len();
self.runs.push((start_ix..end_ix, highlight));
}
}
pub fn build(mut self) -> CodeLabel {
if self.filter_range.end == 0 {
self.respan_filter_range(None);
}
CodeLabel {
text: self.text,
runs: self.runs,
filter_range: self.filter_range,
}
}
}
impl CodeLabel {
pub fn fallback_for_completion(
item: &lsp::CompletionItem,
@@ -2286,22 +2324,36 @@ impl CodeLabel {
}
pub fn plain(text: String, filter_text: Option<&str>) -> Self {
Self::filtered(text, filter_text, Vec::new())
}
pub fn filtered(
text: String,
filter_text: Option<&str>,
runs: Vec<(Range<usize>, HighlightId)>,
) -> Self {
let filter_range = filter_text
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..text.len());
Self {
runs: Vec::new(),
filter_range,
text,
}
Self::new(text, filter_range, runs)
}
pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
let start_ix = self.text.len();
self.text.push_str(text);
let end_ix = self.text.len();
if let Some(highlight) = highlight {
self.runs.push((start_ix..end_ix, highlight));
pub fn new(
text: String,
filter_range: Range<usize>,
runs: Vec<(Range<usize>, HighlightId)>,
) -> Self {
assert!(
text.get(filter_range.clone()).is_some(),
"invalid filter range"
);
runs.iter().for_each(|(range, _)| {
assert!(text.get(range.clone()).is_some(), "invalid run range");
});
Self {
runs,
filter_range,
text,
}
}

View File

@@ -142,8 +142,6 @@ 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.
@@ -578,7 +576,6 @@ 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

@@ -463,11 +463,7 @@ fn build_code_label(
let filter_range = label.filter_range.clone();
text.get(filter_range.clone())?;
Some(CodeLabel {
text,
runs,
filter_range,
})
Some(CodeLabel::new(text, filter_range, runs))
}
fn lsp_completion_to_extension(value: lsp::CompletionItem) -> extension::Completion {
@@ -615,11 +611,7 @@ fn test_build_code_label() {
assert_eq!(
label,
CodeLabel {
text: label_text,
runs: label_runs,
filter_range: label.filter_range.clone()
}
CodeLabel::new(label_text, label.filter_range.clone(), label_runs)
)
}

View File

@@ -188,11 +188,7 @@ impl super::LspAdapter for CLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(detail.len() + 1..text.len());
return Some(CodeLabel {
filter_range,
text,
runs,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
if completion.detail.is_some() =>
@@ -208,11 +204,7 @@ impl super::LspAdapter for CLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(detail.len() + 1..text.len());
return Some(CodeLabel {
filter_range,
text,
runs,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
if completion.detail.is_some() =>
@@ -236,11 +228,7 @@ impl super::LspAdapter for CLspAdapter {
filter_start..filter_end
});
return Some(CodeLabel {
filter_range,
text,
runs,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some(kind) => {
let highlight_name = match kind {
@@ -324,11 +312,11 @@ impl super::LspAdapter for CLspAdapter {
_ => return None,
};
Some(CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
Some(CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
})
language.highlight_text(&text.as_str().into(), display_range),
))
}
fn prepare_initialize_params(

View File

@@ -222,7 +222,7 @@ impl LspAdapter for GoLspAdapter {
Some((lsp::CompletionItemKind::MODULE, detail)) => {
let text = format!("{label} {detail}");
let source = Rope::from(format!("import {text}").as_str());
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
let runs = language.highlight_text(&source, 7..7 + text.len());
let filter_range = completion
.filter_text
.as_deref()
@@ -231,11 +231,7 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some((
lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
@@ -246,7 +242,7 @@ impl LspAdapter for GoLspAdapter {
Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
language.highlight_text(&source, 4..4 + text.len()),
);
let filter_range = completion
.filter_text
@@ -256,18 +252,14 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some((lsp::CompletionItemKind::STRUCT, _)) => {
let text = format!("{label} struct {{}}");
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
@@ -277,18 +269,14 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some((lsp::CompletionItemKind::INTERFACE, _)) => {
let text = format!("{label} interface {{}}");
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
@@ -298,11 +286,7 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some((lsp::CompletionItemKind::FIELD, detail)) => {
let text = format!("{label} {detail}");
@@ -310,7 +294,7 @@ impl LspAdapter for GoLspAdapter {
Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
language.highlight_text(&source, 16..16 + text.len()),
);
let filter_range = completion
.filter_text
@@ -320,11 +304,7 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
if let Some(signature) = detail.strip_prefix("func") {
@@ -332,7 +312,7 @@ impl LspAdapter for GoLspAdapter {
let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
@@ -342,11 +322,7 @@ impl LspAdapter for GoLspAdapter {
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
filter_range,
text,
runs,
});
return Some(CodeLabel::new(text, filter_range, runs));
}
}
_ => {}
@@ -406,11 +382,11 @@ impl LspAdapter for GoLspAdapter {
_ => return None,
};
Some(CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
Some(CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
})
language.highlight_text(&text.as_str().into(), display_range),
))
}
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
@@ -810,15 +786,15 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "Hello(a B) c.D".to_string(),
filter_range: 0..5,
runs: vec![
Some(CodeLabel::new(
"Hello(a B) c.D".to_string(),
0..5,
vec![
(0..5, highlight_function),
(8..9, highlight_type),
(13..14, highlight_type),
],
})
]
))
);
// Nested methods
@@ -834,15 +810,15 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "one.two.Three() [3]interface{}".to_string(),
filter_range: 0..13,
runs: vec![
Some(CodeLabel::new(
"one.two.Three() [3]interface{}".to_string(),
0..13,
vec![
(8..13, highlight_function),
(17..18, highlight_number),
(19..28, highlight_keyword),
],
})
))
);
// Nested fields
@@ -858,11 +834,11 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "two.Three a.Bcd".to_string(),
filter_range: 0..9,
runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
})
Some(CodeLabel::new(
"two.Three a.Bcd".to_string(),
0..9,
vec![(4..9, highlight_field), (12..15, highlight_type)],
))
);
}

View File

@@ -407,11 +407,6 @@ impl LspAdapter for PyrightLspAdapter {
return None;
}
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
let mut text = label.clone();
if let Some(completion_details) = item
.label_details
@@ -420,14 +415,14 @@ impl LspAdapter for PyrightLspAdapter {
{
write!(&mut text, " {}", completion_details).ok();
}
Some(language::CodeLabel {
runs: highlight_id
Some(language::CodeLabel::filtered(
text,
item.filter_text.as_deref(),
highlight_id
.map(|id| (0..label.len(), id))
.into_iter()
.collect(),
text,
filter_range,
})
))
}
async fn label_for_symbol(
@@ -458,11 +453,11 @@ impl LspAdapter for PyrightLspAdapter {
_ => return None,
};
Some(language::CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
Some(language::CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
})
language.highlight_text(&text.as_str().into(), display_range),
))
}
async fn workspace_configuration(
@@ -1424,16 +1419,11 @@ impl LspAdapter for PyLspAdapter {
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
_ => return None,
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
Some(language::CodeLabel {
text: label.clone(),
runs: vec![(0..label.len(), highlight_id)],
filter_range,
})
Some(language::CodeLabel::filtered(
label.clone(),
item.filter_text.as_deref(),
vec![(0..label.len(), highlight_id)],
))
}
async fn label_for_symbol(
@@ -1463,12 +1453,11 @@ impl LspAdapter for PyLspAdapter {
}
_ => return None,
};
Some(language::CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
Some(language::CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
})
language.highlight_text(&text.as_str().into(), display_range),
))
}
async fn workspace_configuration(
@@ -1708,11 +1697,6 @@ impl LspAdapter for BasedPyrightLspAdapter {
return None;
}
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
let mut text = label.clone();
if let Some(completion_details) = item
.label_details
@@ -1721,14 +1705,14 @@ impl LspAdapter for BasedPyrightLspAdapter {
{
write!(&mut text, " {}", completion_details).ok();
}
Some(language::CodeLabel {
runs: highlight_id
Some(language::CodeLabel::filtered(
text,
item.filter_text.as_deref(),
highlight_id
.map(|id| (0..label.len(), id))
.into_iter()
.collect(),
text,
filter_range,
})
))
}
async fn label_for_symbol(
@@ -1758,12 +1742,11 @@ impl LspAdapter for BasedPyrightLspAdapter {
}
_ => return None,
};
Some(language::CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
Some(language::CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
})
language.highlight_text(&text.as_str().into(), display_range),
))
}
async fn workspace_configuration(

View File

@@ -245,11 +245,7 @@ impl LspAdapter for RustLspAdapter {
})
.unwrap_or_else(filter_range);
CodeLabel {
text,
runs,
filter_range,
}
CodeLabel::new(text, filter_range, runs)
};
let mut label = match (detail_right, completion.kind) {
(Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
@@ -400,11 +396,11 @@ impl LspAdapter for RustLspAdapter {
let filter_range = prefix.len()..prefix.len() + name.len();
let display_range = 0..filter_range.end;
Some(CodeLabel {
runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
text: format!("{prefix}{name}"),
Some(CodeLabel::new(
format!("{prefix}{name}"),
filter_range,
})
language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
))
}
fn prepare_initialize_params(
@@ -1202,10 +1198,10 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5,
runs: vec![
Some(CodeLabel::new(
"hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
0..5,
vec![
(0..5, highlight_function),
(7..10, highlight_keyword),
(11..17, highlight_type),
@@ -1213,7 +1209,7 @@ mod tests {
(25..28, highlight_type),
(29..30, highlight_type),
],
})
))
);
assert_eq!(
adapter
@@ -1230,10 +1226,10 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5,
runs: vec![
Some(CodeLabel::new(
"hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
0..5,
vec![
(0..5, highlight_function),
(7..10, highlight_keyword),
(11..17, highlight_type),
@@ -1241,7 +1237,7 @@ mod tests {
(25..28, highlight_type),
(29..30, highlight_type),
],
})
))
);
assert_eq!(
adapter
@@ -1255,11 +1251,11 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "len: usize".to_string(),
filter_range: 0..3,
runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
})
Some(CodeLabel::new(
"len: usize".to_string(),
0..3,
vec![(0..3, highlight_field), (5..10, highlight_type),],
))
);
assert_eq!(
@@ -1278,10 +1274,10 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5,
runs: vec![
Some(CodeLabel::new(
"hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
0..5,
vec![
(0..5, highlight_function),
(7..10, highlight_keyword),
(11..17, highlight_type),
@@ -1289,7 +1285,7 @@ mod tests {
(25..28, highlight_type),
(29..30, highlight_type),
],
})
))
);
assert_eq!(
@@ -1307,10 +1303,10 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
filter_range: 0..5,
runs: vec![
Some(CodeLabel::new(
"hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
0..5,
vec![
(0..5, highlight_function),
(7..10, highlight_keyword),
(11..17, highlight_type),
@@ -1318,7 +1314,7 @@ mod tests {
(25..28, highlight_type),
(29..30, highlight_type),
],
})
))
);
assert_eq!(
@@ -1337,16 +1333,16 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
filter_range: 6..18,
runs: vec![
Some(CodeLabel::new(
"await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
6..18,
vec![
(6..18, HighlightId(2)),
(20..23, HighlightId(1)),
(33..40, HighlightId(0)),
(45..46, HighlightId(0))
],
})
))
);
assert_eq!(
@@ -1367,10 +1363,10 @@ mod tests {
&language
)
.await,
Some(CodeLabel {
text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
filter_range: 7..19,
runs: vec![
Some(CodeLabel::new(
"pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
7..19,
vec![
(0..3, HighlightId(1)),
(4..6, HighlightId(1)),
(7..19, HighlightId(2)),
@@ -1378,7 +1374,7 @@ mod tests {
(34..41, HighlightId(0)),
(46..47, HighlightId(0))
],
})
))
);
assert_eq!(
@@ -1394,11 +1390,11 @@ mod tests {
&language,
)
.await,
Some(CodeLabel {
text: "inner_value: String".to_string(),
filter_range: 6..11,
runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
})
Some(CodeLabel::new(
"inner_value: String".to_string(),
6..11,
vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
))
);
}
@@ -1424,22 +1420,22 @@ mod tests {
adapter
.label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
.await,
Some(CodeLabel {
text: "fn hello".to_string(),
filter_range: 3..8,
runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
})
Some(CodeLabel::new(
"fn hello".to_string(),
3..8,
vec![(0..2, highlight_keyword), (3..8, highlight_function)],
))
);
assert_eq!(
adapter
.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
.await,
Some(CodeLabel {
text: "type World".to_string(),
filter_range: 5..10,
runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
})
Some(CodeLabel::new(
"type World".to_string(),
5..10,
vec![(0..4, highlight_keyword), (5..10, highlight_type)],
))
);
}

View File

@@ -777,16 +777,11 @@ impl LspAdapter for TypeScriptLspAdapter {
} else {
item.label.clone()
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
Some(language::CodeLabel {
Some(language::CodeLabel::filtered(
text,
runs: vec![(0..len, highlight_id)],
filter_range,
})
item.filter_text.as_deref(),
vec![(0..len, highlight_id)],
))
}
async fn initialization_options(

View File

@@ -201,16 +201,11 @@ impl LspAdapter for VtslsLspAdapter {
} else {
item.label.clone()
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
Some(language::CodeLabel {
Some(language::CodeLabel::filtered(
text,
runs: vec![(0..len, highlight_id)],
filter_range,
})
item.filter_text.as_deref(),
vec![(0..len, highlight_id)],
))
}
async fn workspace_configuration(

View File

@@ -292,18 +292,16 @@ impl<'a> MarkdownParser<'a> {
finder.kinds(&[linkify::LinkKind::Url]);
let mut last_link_len = prev_len;
for link in finder.links(t) {
let start = link.start();
let end = link.end();
let range = (prev_len + start)..(prev_len + end);
let start = prev_len + link.start();
let end = prev_len + link.end();
let range = start..end;
link_ranges.push(range.clone());
link_urls.push(link.as_str().to_string());
// If there is a style before we match a link, we have to add this to the highlighted ranges
if style != MarkdownHighlightStyle::default()
&& last_link_len < link.start()
{
if style != MarkdownHighlightStyle::default() && last_link_len < start {
highlights.push((
last_link_len..link.start(),
last_link_len..start,
MarkdownHighlight::Style(style.clone()),
));
}
@@ -376,15 +374,11 @@ impl<'a> MarkdownParser<'a> {
if !text.is_empty() {
let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: source_range.clone(),
contents: text.into(),
highlights: highlights.clone(),
region_ranges: region_ranges.clone(),
regions: regions.clone(),
contents: mem::take(&mut text).into(),
highlights: mem::take(&mut highlights),
region_ranges: mem::take(&mut region_ranges),
regions: mem::take(&mut regions),
});
text = String::new();
highlights = vec![];
region_ranges = vec![];
regions = vec![];
markdown_text_like.push(parsed_regions);
}
image = Image::identify(
@@ -409,9 +403,6 @@ 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));
}
@@ -1280,40 +1271,17 @@ 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,
},)
);
}
#[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,
},)
);
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]

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_16 {
pub(crate) mod m_2025_10_10 {
mod settings;
pub(crate) use settings::restore_code_actions_on_format;
pub(crate) use settings::remove_code_actions_on_format;
}

View File

@@ -0,0 +1,70 @@
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

@@ -1,71 +0,0 @@
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_16::restore_code_actions_on_format),
MigrationType::Json(migrations::m_2025_10_10::remove_code_actions_on_format),
];
run_migrations(text, migrations)
}
@@ -367,7 +367,6 @@ 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);
@@ -1342,11 +1341,7 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_array() {
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
assert_migrate_settings(
&r#"{
"formatter": [
{
@@ -1373,11 +1368,7 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_basic_object() {
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
assert_migrate_settings(
&r#"{
"formatter": {
"code_actions": {
@@ -1509,11 +1500,7 @@ mod tests {
#[test]
fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
{
assert_migrate_settings_with_migrations(
&[MigrationType::TreeSitter(
migrations::m_2025_10_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_10_01,
)],
assert_migrate_settings(
&r#"{
"formatter": {
"code_actions": {
@@ -1929,109 +1916,300 @@ mod tests {
}
#[test]
fn test_restore_code_actions_on_format() {
fn test_code_actions_on_format_migration_basic() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": {
"code_action": "foo"
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": true
}
}"#
.unindent(),
Some(
&r#"{
"code_actions_on_format": {
"foo": true
}
"formatter": [
{
"code_action": "source.organizeImports"
},
{
"code_action": "source.fixAll"
}
]
}
"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_filters_false_values() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": [
{ "code_action": "foo" },
"auto"
]
"code_actions_on_format": {
"a": true,
"b": false,
"c": true
}
}"#
.unindent(),
None,
Some(
&r#"{
"formatter": [
{
"code_action": "a"
},
{
"code_action": "c"
}
]
}
"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_migration_with_existing_formatter_object() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"formatter": {
"code_action": "foo"
"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
}
},
"code_actions_on_format": {
"bar": true,
"baz": false
"Python": {
"code_actions_on_format": {
"source.organizeImports": true,
"source.fixAll": false
}
}
}
}"#
.unindent(),
Some(
&r#"{
"code_actions_on_format": {
"foo": true,
"bar": true,
"baz": false
}
"formatter": [
{
"code_action": "source.fixAll"
},
"prettier"
],
"languages": {
"Rust": {
"formatter": [
{
"code_action": "source.organizeImports"
},
"rust-analyzer"
]
},
"Python": {
"formatter": [
{
"code_action": "source.organizeImports"
}
]
}
}
}"#
.unindent(),
),
);
}
#[test]
fn test_code_actions_on_format_no_migration_when_not_present() {
assert_migrate_settings_with_migrations(
&[MigrationType::Json(
migrations::m_2025_10_16::restore_code_actions_on_format,
migrations::m_2025_10_10::remove_code_actions_on_format,
)],
&r#"{
"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
}
"formatter": ["prettier"]
}"#
.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,5 +10,4 @@ pub(crate) use settings::{
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
migrate_language_setting,
};

View File

@@ -108,24 +108,3 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
(#eq? @agent1 "agent")
(#eq? @agent2 "agent")
)"#;
/// Migrate language settings,
/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
/// Fails early if `migrate_fn` returns an error at any point
pub fn migrate_language_setting(
value: &mut serde_json::Value,
migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
migrate_fn(value, &[])?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let path = vec!["languages", language_name];
migrate_fn(language, &path)?;
}
}
Ok(())
}

View File

@@ -6165,20 +6165,22 @@ 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();
locators.push(Locator::max());
if let Some(mapping) = self.excerpt_ids.last() {
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 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 cursor = self.excerpt_ids.cursor::<ExcerptId>(());
for id in sorted_ids {

View File

@@ -577,6 +577,7 @@ fn get_or_npm_install_builtin_agent(
package_name: SharedString,
entrypoint_path: PathBuf,
minimum_version: Option<semver::Version>,
channel: &'static str,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
fs: Arc<dyn Fs>,
@@ -648,10 +649,12 @@ fn get_or_npm_install_builtin_agent(
let dir = dir.clone();
let fs = fs.clone();
async move {
// TODO remove the filter
let latest_version = node_runtime
.npm_package_latest_version(&package_name)
.await
.ok();
.ok()
.filter(|_| channel == "latest");
if let Some(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
@@ -660,6 +663,7 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
)
.await
.log_err();
@@ -683,6 +687,7 @@ fn get_or_npm_install_builtin_agent(
dir.clone(),
node_runtime,
package_name.clone(),
channel,
))
.await?
.into()
@@ -731,13 +736,14 @@ async fn download_latest_version(
dir: PathBuf,
node_runtime: NodeRuntime,
package_name: SharedString,
channel: &'static str,
) -> Result<String> {
log::debug!("downloading latest version of {package_name}");
let tmp_dir = tempfile::tempdir_in(&dir)?;
node_runtime
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.npm_install_packages(tmp_dir.path(), &[(&package_name, channel)])
.await?;
let version = node_runtime
@@ -880,12 +886,17 @@ impl ExternalAgentServer for LocalGemini {
GEMINI_NAME.into(),
"@google/gemini-cli".into(),
"node_modules/@google/gemini-cli/dist/index.js".into(),
// TODO remove these windows-specific workarounds once v0.9.0 stable is released
if cfg!(windows) {
// v0.8.x on Windows has a bug that causes the initialize request to hang forever
Some("0.9.0".parse().unwrap())
Some("0.9.0-preview.4".parse().unwrap())
} else {
Some("0.2.1".parse().unwrap())
},
if cfg!(windows) {
"0.9.0-preview.4"
} else {
"latest"
},
status_tx,
new_version_available_tx,
fs,
@@ -969,6 +980,7 @@ impl ExternalAgentServer for LocalClaudeCode {
"@zed-industries/claude-code-acp".into(),
"node_modules/@zed-industries/claude-code-acp/dist/index.js".into(),
Some("0.5.2".parse().unwrap()),
"latest",
status_tx,
new_version_available_tx,
fs,

View File

@@ -14,7 +14,6 @@ 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,
@@ -988,7 +987,6 @@ 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);
@@ -1335,32 +1333,6 @@ 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) => {
@@ -1368,11 +1340,6 @@ 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 {
@@ -9398,11 +9365,7 @@ impl LspStore {
name: symbol.name,
kind: symbol.kind,
range: symbol.range,
label: CodeLabel {
text: Default::default(),
runs: Default::default(),
filter_range: Default::default(),
},
label: CodeLabel::default(),
},
cx,
)
@@ -9592,11 +9555,7 @@ impl LspStore {
new_text: completion.new_text,
source: completion.source,
documentation: None,
label: CodeLabel {
text: Default::default(),
runs: Default::default(),
filter_range: Default::default(),
},
label: CodeLabel::default(),
insert_text_mode: None,
icon_path: None,
confirm: None,
@@ -12783,7 +12742,6 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
return Ok(None);
}
#[cfg(not(target_os = "windows"))]
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
let mut worktree_abs_path = self.worktree_root_path().to_path_buf();
if self.fs.is_file(&worktree_abs_path).await {
@@ -12793,14 +12751,6 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok()
}
#[cfg(target_os = "windows")]
async fn which(&self, command: &OsStr) -> Option<PathBuf> {
// todo(windows) Getting the shell env variables in a current directory on Windows is more complicated than other platforms
// there isn't a 'default shell' necessarily. The closest would be the default profile on the windows terminal
// SEE: https://learn.microsoft.com/en-us/windows/terminal/customize-settings/startup
which::which(command).ok()
}
async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> {
let mut working_dir = self.worktree_root_path().to_path_buf();
if self.fs.is_file(&working_dir).await {
@@ -13102,19 +13052,19 @@ mod tests {
#[test]
fn test_multi_len_chars_normalization() {
let mut label = CodeLabel {
text: "myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(),
runs: vec![(0..6, HighlightId(1))],
filter_range: 0..6,
};
let mut label = CodeLabel::new(
"myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(),
0..6,
vec![(0..6, HighlightId(1))],
);
ensure_uniform_list_compatible_label(&mut label);
assert_eq!(
label,
CodeLabel {
text: "myElˇ (parameter) myElˇ: { foo: string; }".to_string(),
runs: vec![(0..6, HighlightId(1))],
filter_range: 0..6,
}
CodeLabel::new(
"myElˇ (parameter) myElˇ: { foo: string; }".to_string(),
0..6,
vec![(0..6, HighlightId(1))],
)
);
}
}

View File

@@ -1,124 +0,0 @@
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, bail};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use fs::{Fs, copy_recursive};
use futures::{
@@ -1194,16 +1194,6 @@ 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")?;
@@ -1227,15 +1217,6 @@ 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")
})??;
@@ -1256,18 +1237,6 @@ 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, NotifyResultExt, NotifyTaskExt},
notifications::{DetachAndPromptErr, NotifyTaskExt},
};
use worktree::CreatedEntry;
use zed_actions::workspace::OpenWithSystem;
@@ -2677,14 +2677,12 @@ impl ProjectPanel {
for task in paste_tasks {
match task {
PasteTask::Rename(task) => {
if let Some(CreatedEntry::Included(entry)) =
task.await.notify_async_err(cx)
{
if let Some(CreatedEntry::Included(entry)) = task.await.log_err() {
last_succeed = Some(entry);
}
}
PasteTask::Copy(task) => {
if let Some(Some(entry)) = task.await.notify_async_err(cx) {
if let Some(Some(entry)) = task.await.log_err() {
last_succeed = Some(entry);
}
}

View File

@@ -82,7 +82,7 @@ impl WslRemoteConnection {
this.can_exec = this.detect_can_exec(shell).await?;
this.platform = this.detect_platform(shell).await?;
this.remote_binary_path = Some(
this.ensure_server_binary(&delegate, release_channel, version, commit, shell, cx)
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
.await?,
);
log::debug!("Detected WSL environment: {this:#?}");
@@ -163,7 +163,6 @@ impl WslRemoteConnection {
release_channel: ReleaseChannel,
version: SemanticVersion,
commit: Option<AppCommitSha>,
shell: ShellKind,
cx: &mut AsyncApp,
) -> Result<Arc<RelPath>> {
let version_str = match release_channel {
@@ -185,13 +184,9 @@ impl WslRemoteConnection {
paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
if let Some(parent) = dst_path.parent() {
let parent = parent.display(PathStyle::Posix);
if shell == ShellKind::Nushell {
self.run_wsl_command("mkdir", &[&parent]).await
} else {
self.run_wsl_command("mkdir", &["-p", &parent]).await
}
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
self.run_wsl_command("mkdir", &["-p", &parent.display(PathStyle::Posix)])
.await
.map_err(|e| anyhow!("Failed to create directory: {}", e))?;
}
#[cfg(debug_assertions)]

View File

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

View File

@@ -30,6 +30,13 @@ impl Rope {
Self::default()
}
/// Checks that `index`-th byte is the first byte in a UTF-8 code point
/// sequence or the end of the string.
///
/// The start and end of the string (when `index == self.len()`) are
/// considered to be boundaries.
///
/// Returns `false` if `index` is greater than `self.len()`.
pub fn is_char_boundary(&self, offset: usize) -> bool {
if self.chunks.is_empty() {
return offset == 0;
@@ -673,6 +680,12 @@ impl<'a> Chunks<'a> {
chunks.seek(&range.start, Bias::Right);
range.start
};
let chunk_offset = offset - chunks.start();
if let Some(chunk) = chunks.item()
&& !chunk.text.is_char_boundary(chunk_offset)
{
panic!("byte index {} is not a char boundary", offset);
}
Self {
chunks,
range,

View File

@@ -56,7 +56,7 @@ impl Into<BaseKeymapContent> for BaseKeymap {
impl Display for BaseKeymap {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
BaseKeymap::VSCode => write!(f, "VSCode"),
BaseKeymap::VSCode => write!(f, "VS Code"),
BaseKeymap::JetBrains => write!(f, "JetBrains"),
BaseKeymap::SublimeText => write!(f, "Sublime Text"),
BaseKeymap::Atom => write!(f, "Atom"),
@@ -71,7 +71,7 @@ impl Display for BaseKeymap {
impl BaseKeymap {
#[cfg(target_os = "macos")]
pub const OPTIONS: [(&'static str, Self); 7] = [
("VSCode (Default)", Self::VSCode),
("VS Code (Default)", Self::VSCode),
("Atom", Self::Atom),
("JetBrains", Self::JetBrains),
("Sublime Text", Self::SublimeText),
@@ -82,7 +82,7 @@ impl BaseKeymap {
#[cfg(not(target_os = "macos"))]
pub const OPTIONS: [(&'static str, Self); 6] = [
("VSCode (Default)", Self::VSCode),
("VS Code (Default)", Self::VSCode),
("Atom", Self::Atom),
("JetBrains", Self::JetBrains),
("Sublime Text", Self::SublimeText),

View File

@@ -318,11 +318,6 @@ 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("Buffer Font"),
SettingsPageItem::SectionHeader("Fonts"),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Family",
title: "Buffer 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: "Font Size",
title: "Buffer 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: "Font Weight",
title: "Buffer 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: "Line Height",
title: "Buffer 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: "Font Features",
title: "Buffer 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: "Font Fallbacks",
title: "Buffer Font Fallbacks",
description: "The font fallbacks to use for rendering in text buffers.",
field: Box::new(
SettingField {
@@ -331,9 +331,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("UI Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Family",
title: "UI Font Family",
description: "Font family for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_family,
@@ -343,7 +342,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Size",
title: "UI Font Size",
description: "Font size for UI elements",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_size,
@@ -353,7 +352,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Font Weight",
title: "UI Font Weight",
description: "Font weight for UI elements (100-900)",
field: Box::new(SettingField {
pick: |settings_content| &settings_content.theme.ui_font_weight,
@@ -364,7 +363,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Font Features",
title: "UI Font Features",
description: "The OpenType features to enable for rendering in UI elements.",
field: Box::new(
SettingField {
@@ -379,7 +378,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
SettingsPageItem::SettingItem(SettingItem {
files: USER,
title: "Font Fallbacks",
title: "UI Font Fallbacks",
description: "The font fallbacks to use for rendering in the UI.",
field: Box::new(
SettingField {
@@ -392,9 +391,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
),
metadata: None,
}),
SettingsPageItem::SectionHeader("Agent Panel Font"),
SettingsPageItem::SettingItem(SettingItem {
title: "UI Font Size",
title: "Agent Panel 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| {
@@ -410,7 +408,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Buffer Font Size",
title: "Agent Panel 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,
@@ -421,7 +419,6 @@ 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",
@@ -434,6 +431,7 @@ 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",
@@ -810,9 +808,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Hover Popover"),
SettingsPageItem::SectionHeader("Hover"),
SettingsPageItem::SettingItem(SettingItem {
title: "Enabled",
title: "Hover Popover 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,
@@ -825,7 +823,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
}),
// todo(settings ui): add units to this number input
SettingsPageItem::SettingItem(SettingItem {
title: "Delay",
title: "Hover Popover 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,
@@ -836,9 +834,21 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SectionHeader("Drag And Drop Selection"),
SettingsPageItem::SectionHeader("Code Actions & Selection"),
SettingsPageItem::SettingItem(SettingItem {
title: "Enabled",
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",
description: "Enable drag and drop selection",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -862,7 +872,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Delay",
title: "Drag And Drop Selection Delay",
description: "Delay in milliseconds before drag and drop selection starts",
field: Box::new(SettingField {
pick: |settings_content| {
@@ -1004,18 +1014,6 @@ 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",
@@ -5416,27 +5414,6 @@ 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::{Settings, SettingsContent, SettingsStore};
use settings::{SettingsContent, SettingsStore};
use std::{
any::{Any, TypeId, type_name},
cell::RefCell,
@@ -466,13 +466,6 @@ 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 {
@@ -485,8 +478,8 @@ pub fn open_settings_editor(
is_movable: true,
kind: gpui::WindowKind::Floating,
window_background: cx.theme().window_background_appearance(),
window_min_size: Some(scaled_bounds),
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),
..Default::default()
},
|window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
@@ -1702,7 +1695,7 @@ impl SettingsWindow {
};
v_flex()
.w_56()
.w_64()
.p_2p5()
.when(cfg!(target_os = "macos"), |c| c.pt_10())
.h_full()
@@ -2137,7 +2130,7 @@ impl SettingsWindow {
}
return v_flex()
.flex_1()
.size_full()
.pt_6()
.pb_8()
.px_8()

View File

@@ -2400,7 +2400,9 @@ impl BufferSnapshot {
} else if bias == Bias::Right && offset == self.len() {
Anchor::MAX
} else {
if !self.visible_text.is_char_boundary(offset) {
if offset > self.visible_text.len() {
panic!("offset {} is out of bounds", offset)
} else if !self.visible_text.is_char_boundary(offset) {
// find the character
let char_start = self.visible_text.floor_char_boundary(offset);
// `char_start` must be less than len and a char boundary

View File

@@ -15,16 +15,9 @@ 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: label.into(),
highlight_indices,
}
}

View File

@@ -369,8 +369,6 @@ 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);
@@ -401,18 +399,13 @@ impl Vim {
selections.push(selection);
}
// 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() {
if row == head.row() {
break;
}
// Find the next or previous buffer row where the `row` should
// be moved to, so that wrapped lines are skipped.
// Move to the next or previous buffer row, ensuring that
// wrapped lines are handled correctly.
let direction = if tail.row() > head.row() { -1 } else { 1 };
row = 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.6"
version = "0.210.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
stable
dev

View File

@@ -33,7 +33,7 @@ pub struct EditPrediction {
pub snapshot: BufferSnapshot,
pub edit_preview: EditPreview,
// We keep a reference to the buffer so that we do not need to reload it from disk when applying the prediction.
_buffer: Entity<Buffer>,
pub buffer: Entity<Buffer>,
}
impl EditPrediction {
@@ -108,7 +108,7 @@ impl EditPrediction {
edits,
snapshot,
edit_preview,
_buffer: buffer,
buffer,
})
}
@@ -184,6 +184,10 @@ pub fn interpolate_edits(
if edits.is_empty() { None } else { Some(edits) }
}
pub fn line_range_to_point_range(range: Range<predict_edits_v3::Line>) -> Range<language::Point> {
language::Point::new(range.start.0, 0)..language::Point::new(range.end.0, 0)
}
fn edits_from_response(
edits: &[predict_edits_v3::Edit],
snapshot: &TextBufferSnapshot,
@@ -191,12 +195,14 @@ fn edits_from_response(
edits
.iter()
.flat_map(|edit| {
let old_text = snapshot.text_for_range(edit.range.clone());
let point_range = line_range_to_point_range(edit.range.clone());
let offset = point_range.to_offset(snapshot).start;
let old_text = snapshot.text_for_range(point_range);
excerpt_edits_from_response(
old_text.collect::<Cow<str>>(),
&edit.content,
edit.range.start,
offset,
&snapshot,
)
})
@@ -252,6 +258,7 @@ mod tests {
use super::*;
use cloud_llm_client::predict_edits_v3;
use edit_prediction_context::Line;
use gpui::{App, Entity, TestAppContext, prelude::*};
use indoc::indoc;
use language::{Buffer, ToOffset as _};
@@ -278,7 +285,7 @@ mod tests {
// TODO cover more cases when multi-file is supported
let big_edits = vec![predict_edits_v3::Edit {
path: PathBuf::from("test.txt").into(),
range: 0..old.len(),
range: Line(0)..Line(old.lines().count() as u32),
content: new.into(),
}];
@@ -317,7 +324,7 @@ mod tests {
edits,
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
path: Path::new("test.txt").into(),
_buffer: buffer.clone(),
buffer: buffer.clone(),
edit_preview,
};

View File

@@ -17,8 +17,8 @@ use gpui::{
App, Entity, EntityId, Global, SemanticVersion, SharedString, Subscription, Task, WeakEntity,
http_client, prelude::*,
};
use language::BufferSnapshot;
use language::{Buffer, DiagnosticSet, LanguageServerId, ToOffset as _, ToPoint};
use language::{BufferSnapshot, TextBufferSnapshot};
use language_model::{LlmApiToken, RefreshLlmTokenListener};
use project::Project;
use release_channel::AppVersion;
@@ -106,30 +106,40 @@ struct ZetaProject {
current_prediction: Option<CurrentEditPrediction>,
}
#[derive(Clone)]
#[derive(Debug, Clone)]
struct CurrentEditPrediction {
pub requested_by_buffer_id: EntityId,
pub prediction: EditPrediction,
}
impl CurrentEditPrediction {
fn should_replace_prediction(
&self,
old_prediction: &Self,
snapshot: &TextBufferSnapshot,
) -> bool {
if self.requested_by_buffer_id != old_prediction.requested_by_buffer_id {
fn should_replace_prediction(&self, old_prediction: &Self, cx: &App) -> bool {
let Some(new_edits) = self
.prediction
.interpolate(&self.prediction.buffer.read(cx))
else {
return false;
};
if self.prediction.buffer != old_prediction.prediction.buffer {
return true;
}
let Some(old_edits) = old_prediction.prediction.interpolate(snapshot) else {
let Some(old_edits) = old_prediction
.prediction
.interpolate(&old_prediction.prediction.buffer.read(cx))
else {
return true;
};
let Some(new_edits) = self.prediction.interpolate(snapshot) else {
return false;
};
if old_edits.len() == 1 && new_edits.len() == 1 {
// This reduces the occurrence of UI thrash from replacing edits
//
// TODO: This is fairly arbitrary - should have a more general heuristic that handles multiple edits.
if self.requested_by_buffer_id == self.prediction.buffer.entity_id()
&& self.requested_by_buffer_id == old_prediction.prediction.buffer.entity_id()
&& old_edits.len() == 1
&& new_edits.len() == 1
{
let (old_range, old_text) = &old_edits[0];
let (new_range, new_text) = &new_edits[0];
new_range == old_range && new_text.starts_with(old_text)
@@ -421,8 +431,7 @@ impl Zeta {
.current_prediction
.as_ref()
.is_none_or(|old_prediction| {
new_prediction
.should_replace_prediction(&old_prediction, buffer.read(cx))
new_prediction.should_replace_prediction(&old_prediction, cx)
})
{
project_state.current_prediction = Some(new_prediction);
@@ -926,7 +935,7 @@ fn make_cloud_request(
referenced_declarations.push(predict_edits_v3::ReferencedDeclaration {
path: path.as_std_path().into(),
text: text.into(),
range: snippet.declaration.item_range(),
range: snippet.declaration.item_line_range(),
text_is_truncated,
signature_range: snippet.declaration.signature_range_in_item_text(),
parent_index,
@@ -954,8 +963,12 @@ fn make_cloud_request(
predict_edits_v3::PredictEditsRequest {
excerpt_path,
excerpt: context.excerpt_text.body,
excerpt_line_range: context.excerpt.line_range,
excerpt_range: context.excerpt.range,
cursor_offset: context.cursor_offset_in_excerpt,
cursor_point: predict_edits_v3::Point {
line: predict_edits_v3::Line(context.cursor_point.row),
column: context.cursor_point.column,
},
referenced_declarations,
signatures,
excerpt_parent,
@@ -992,7 +1005,7 @@ fn add_signature(
text: text.into(),
text_is_truncated,
parent_index,
range: parent_declaration.signature_range(),
range: parent_declaration.signature_line_range(),
});
declaration_to_signature_index.insert(declaration_id, signature_index);
Some(signature_index)
@@ -1007,7 +1020,8 @@ mod tests {
use client::UserStore;
use clock::FakeSystemClock;
use cloud_llm_client::predict_edits_v3;
use cloud_llm_client::predict_edits_v3::{self, Point};
use edit_prediction_context::Line;
use futures::{
AsyncReadExt, StreamExt,
channel::{mpsc, oneshot},
@@ -1067,7 +1081,7 @@ mod tests {
request_id: Uuid::new_v4(),
edits: vec![predict_edits_v3::Edit {
path: Path::new(path!("root/1.txt")).into(),
range: 0..snapshot1.len(),
range: Line(0)..Line(snapshot1.max_point().row + 1),
content: "Hello!\nHow are you?\nBye".into(),
}],
debug_info: None,
@@ -1083,7 +1097,6 @@ mod tests {
});
// Prediction for another file
let prediction_task = zeta.update(cx, |zeta, cx| {
zeta.refresh_prediction(&project, &buffer1, position, cx)
});
@@ -1093,14 +1106,13 @@ mod tests {
request_id: Uuid::new_v4(),
edits: vec![predict_edits_v3::Edit {
path: Path::new(path!("root/2.txt")).into(),
range: 0..snapshot1.len(),
range: Line(0)..Line(snapshot1.max_point().row + 1),
content: "Hola!\nComo estas?\nAdios".into(),
}],
debug_info: None,
})
.unwrap();
prediction_task.await.unwrap();
zeta.read_with(cx, |zeta, cx| {
let prediction = zeta
.current_prediction_for_buffer(&buffer1, &project, cx)
@@ -1159,14 +1171,20 @@ mod tests {
request.excerpt_path.as_ref(),
Path::new(path!("root/foo.md"))
);
assert_eq!(request.cursor_offset, 10);
assert_eq!(
request.cursor_point,
Point {
line: Line(1),
column: 3
}
);
respond_tx
.send(predict_edits_v3::PredictEditsResponse {
request_id: Uuid::new_v4(),
edits: vec![predict_edits_v3::Edit {
path: Path::new(path!("root/foo.md")).into(),
range: 0..snapshot.len(),
range: Line(0)..Line(snapshot.max_point().row + 1),
content: "Hello!\nHow are you?\nBye".into(),
}],
debug_info: None,
@@ -1244,7 +1262,7 @@ mod tests {
request_id: Uuid::new_v4(),
edits: vec![predict_edits_v3::Edit {
path: Path::new(path!("root/foo.md")).into(),
range: 0..snapshot.len(),
range: Line(0)..Line(snapshot.max_point().row + 1),
content: "Hello!\nHow are you?\nBye".into(),
}],
debug_info: None,

View File

@@ -98,10 +98,11 @@ struct Zeta2Args {
#[derive(clap::ValueEnum, Default, Debug, Clone)]
enum PromptFormat {
#[default]
MarkedExcerpt,
LabeledSections,
OnlySnippets,
#[default]
NumberedLines,
}
impl Into<predict_edits_v3::PromptFormat> for PromptFormat {
@@ -110,6 +111,7 @@ impl Into<predict_edits_v3::PromptFormat> for PromptFormat {
Self::MarkedExcerpt => predict_edits_v3::PromptFormat::MarkedExcerpt,
Self::LabeledSections => predict_edits_v3::PromptFormat::LabeledSections,
Self::OnlySnippets => predict_edits_v3::PromptFormat::OnlySnippets,
Self::NumberedLines => predict_edits_v3::PromptFormat::NumberedLines,
}
}
}

View File

@@ -97,7 +97,60 @@ To ensure you're using your billing method of choice, [open a new Claude Code th
The first time you create a Claude Code thread, Zed will install [@zed-industries/claude-code-acp](https://github.com/zed-industries/claude-code-acp). This installation is only available to Zed and is kept up to date as you use the agent.
Zed will always use this managed version of Claude Code even if you have it installed globally.
Zed will always use this managed version of the Claude Code adapter, which includes a vendored version of the Claude Code CLI, even if you have it installed globally.
If you want to override the executable used by the adapter, you can set the `CLAUDE_CODE_EXECUTABLE` environment variable in your settings to the path of your preferred executable.
```json
{
"agent_servers": {
"claude": {
"env": {
"CLAUDE_CODE_EXECUTABLE": "/path/to/alternate-claude-code-executable"
}
}
}
}
```
## Codex CLI
You can also run [Codex CLI](https://github.com/openai/codex) directly via Zed's [agent panel](./agent-panel.md).
Under the hood, Zed runs Codex CLI and communicates to it over ACP, through [a dedicated adapter](https://github.com/zed-industries/codex-acp).
### Getting Started
As of Zed Stable v0.208 you should be able to use Codex directly from Zed. Open the agent panel with {#kb agent::ToggleFocus}, and then use the `+` button in the top right to start a new Codex thread.
If you'd like to bind this to a keyboard shortcut, you can do so by editing your `keymap.json` file via the `zed: open keymap` command to include:
```json
[
{
"bindings": {
"cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "codex" }]
}
}
]
```
### Authentication
Authentication to Zed's Codex installation is decoupled entirely from Zed's agent. That is to say, an OpenAI API key added via the [Zed Agent's settings](./llm-providers.md#openai) will _not_ be utilized by Codex for authentication and billing.
To ensure you're using your billing method of choice, [open a new Codex thread](./agent-panel.md#new-thread). The first time you will be prompted to authenticate with one of three methods:
1. Login with ChatGPT - allows you to use your existing, paid ChatGPT subscription. _Note: This method isn't currently supported in remote projects_
2. `CODEX_API_KEY` - uses an API key you have set in your environment under the variable `CODEX_API_KEY`.
3. `OPENAI_API_KEY` - uses an API key you have set in your environment under the variable `OPENAI_API_KEY`.
If you are already logged in and want to change your authentication method, type `/logout` in the thread and authenticate again.
#### Installation
The first time you create a Codex thread, Zed will install [codex-acp](https://github.com/zed-industries/codex-acp). This installation is only available to Zed and is kept up to date as you use the agent.
Zed will always use this managed version of Codex even if you have it installed globally.
### Usage

View File

@@ -40,9 +40,6 @@ Were working hard to expand the models supported by Zeds subscription offe
| Grok 4 Fast | X.ai | Input | $0.20 | $0.22 |
| | X.ai | Output | $0.50 | $0.55 |
| | X.ai | Cached Input | $0.05 | $0.055 |
| Grok 4 Fast | X.ai | Input | $0.20 | $0.22 |
| | X.ai | Output | $0.50 | $0.55 |
| | X.ai | Cached Input | $0.05 | $0.055 |
| Grok 4 (Non-Reasoning) | X.ai | Input | $0.20 | $0.22 |
| | X.ai | Output | $0.50 | $0.55 |
| | X.ai | Cached Input | $0.05 | $0.055 |

View File

@@ -40,7 +40,7 @@ This is mostly a formality on Wednesday's minor update releases, but can be bene
1. Check the release assets.
- Ensure the stable and preview release jobs have finished without error.
- Ensure each draft has the proper number of assets—releases currently have 10 assets each.
- Ensure each draft has the proper number of assets—releases currently have 11 assets each.
- Download the artifacts for each release draft and test that you can run them locally.
1. Publish the drafts.

View File

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

View File