Compare commits

...

21 Commits

Author SHA1 Message Date
David Kleingeld
dfeb67f93c enjoy
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-11-07 13:31:43 +01:00
Danilo Leal
0f5a63a9b0 agent_ui: Make "waiting confirmation" state more apparent (#41998)
This PR changes the loading/generating indicator when in the "waiting
for tool call confirmation" state so that's a bit more visible and
discernible as needing your attention, as opposed to a regular
generating state.

<img width="400" alt="Screenshot 2025-11-05 at 10  46@2x"
src="https://github.com/user-attachments/assets/88adbf97-20fb-49c4-9c77-b0a3a22aa14e"
/>

Release Notes:

- agent: Improved the "waiting for confirmation" state visibility so
that you more rapidly know the agent is waiting for you to act.
2025-11-05 11:45:44 -03:00
Danilo Leal
c8ada5b1ae agent_ui: Reduce label repetitiveness on new thread menu (#42001)
Mostly just removing "thread" from all external agent menu items; I
think we can do without it and it already becomes much better/cleaner.

Release Notes:

- N/A
2025-11-05 11:45:31 -03:00
Techy
27a18843d4 open_ai: Make the deltas optional (#39142)
I am using an Azure OpenAI instance since that is what is provided at
work and with how they have it setup not all responses contain a delta,
which lead to errors and truncated responses. This is related to how
they are filtering potentially offensive requests and responses. I don't
believe this filter was made in-house, instead I believe it is provided
by Microsoft/Azure, so I suspect this fix may help other users.

Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-05 13:47:14 +01:00
Smit Barmase
2bc1d60c52 remote: Fix open terminal fails when $SHELL is not set (#41990)
Closes #41644

Release Notes:

- Fixed issue where it failed to spawn terminal on systems such as
Alpine.
2025-11-05 18:15:15 +05:30
Karl-Erik Enkelmann
17933f1222 Update documentation on Java support (#41758)
This brings the documentation on Java in line with the much changed
reality of the Java extension.
Note that the correctness of this is contingent on
https://github.com/zed-industries/extensions/pull/3745 being merged.

Release Notes:

- N/A
2025-11-05 12:24:29 +01:00
Vitaly Slobodin
cd87307289 language: Fix language detection for injected syntax layers (#41111)
Closes #40632

**TL;DR:** The `wrap selections in tag` action was unavailable in ERB
files, even when the cursor was positioned in HTML content (outside of
Ruby code blocks). This happened because `syntax_layer_at()` incorrectly
returned the Ruby language for positions that were actually in HTML.
**NOTE:** I am not familiar with that part of Zed so it could be that
the fix here is completely incorrect.

Previously, `syntax_layer_at` incorrectly reported injected languages
(e.g., Ruby in ERB files) even when the cursor was in the base language
content (HTML). This broke actions like `wrap selections in tag` that
depend on language-specific configuration.

The issue had two parts:
1. Missing start boundary check: The filter only checked if a layer's
end was after the cursor (`end_byte() > offset`), not if it started
before, causing layers outside the cursor position to be included. See
the `BEFORE` video: when I click on the HTML part it reports `Ruby`
language instead of `HTML`.
2. Wrong boundary reference for injections: For injected layers with
`included_sub_ranges` (like Ruby code blocks in ERB), checking the root
node boundaries returned the entire file range instead of the actual
injection ranges.

This fix:
- Adds the containment check using half-open range semantics [start,
end) for root node boundaries. That ensures proper reporting of the
detected language when a cursor (`|`) is located right after the
injection:

   ```
   <body>
    <%= yield %>|
  </body>
   ```

- Checks `included_sub_ranges` for injected layers to determine if the
cursor is actually within an injection
- Falls back to root node boundaries for base layers without sub-ranges.
This is the original behavior.

Fixes ERB language support where actions should be available based on
the cursor's actual language context. I think that also applies to some
other template languages like HEEX (Phoenix) and `*.pug`. On short
videos below you can see how I navigate through the ERB template and the
terminal on the right outputs the detected language if you apply the
following patch:

```diff
diff --git i/crates/editor/src/editor.rs w/crates/editor/src/editor.rs
index 15af61f5d2..54a8e0ae37 100644
--- i/crates/editor/src/editor.rs
+++ w/crates/editor/src/editor.rs
@@ -10671,6 +10671,7 @@ impl Editor {
         for selection in self.selections.disjoint_anchors_arc().iter() {
             if snapshot
                 .language_at(selection.start)
+                .inspect(|language| println!("Detected language: {:?}", language))
                 .and_then(|lang| lang.config().wrap_characters.as_ref())
                 .is_some()
             {
```

**Before:**


https://github.com/user-attachments/assets/3f8358f4-d343-462e-b6b1-3f1f2e8c533d


**After:**



https://github.com/user-attachments/assets/c1b9f065-1b44-45a2-8a24-76b7d812130d



Here is the ERB template:

```
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
      /* Email styles need to be inline */
    </style>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
```

Release Notes:

- N/A
2025-11-05 12:16:28 +01:00
Vitaly Slobodin
11b29d693f ruby: Add note about enabling Ruby LSP for ERB files (#41851)
Hi, this is a follow-up change for
https://github.com/zed-industries/zed/pull/41754 I think it important to
keep existing things working. So add notes to the Ruby extension doc
about enabling Ruby LSP for ERB files as well. Thanks!

Release Notes:

- N/A
2025-11-05 11:00:12 +01:00
Lukas Wirth
c061698229 project: Fetch latest lsp data in deduplicate_range_based_lsp_requests (#41971)
Fixes ZED-2MK

Release Notes:

- Fixed a panic in inlay hints
2025-11-05 08:30:22 +00:00
John Tur
b4f7af066e Work around codegen bug with GetKeyboardState (#41970)
Works around an issue, which can be reproduced in the following program:
```rs
use windows::Win32::UI::Input::KeyboardAndMouse::{GetKeyboardState, VK_CONTROL};

fn main() {
    let mut keyboard_state = [0u8; 256];
    unsafe {
        GetKeyboardState(&mut keyboard_state).unwrap();
    }

    let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0;
    println!("Is Ctrl down: {ctrl_down}");
}
```

In debug mode, this program prints the correct answer. In release mode,
it always prints false. The optimizer appears to think that
`keyboard_state` isn't mutated and remains zeroed, and folds the
`modifier_down` comparisons to `false`.

Release Notes:

- N/A
2025-11-05 08:06:14 +00:00
Smit Barmase
c83621fa1f editor: Fix setting multi_cursor_modifier opens implementation in new pane instead of new tab (#41963)
Closes #41014

Release Notes:

- Fixed an issue where `multi_cursor_modifier` set to `cmd_or_ctrl`
opens implementation in new pane instead of new tab.
2025-11-05 11:31:56 +05:30
Richard Feldman
0da52d6774 Add ACP terminal-login via _meta field (#41954)
As discussed with @benbrandt and @mikayla-maki:

* We now tell ACP clients we support the nonstandard `terminal-auth`
`_meta` field for terminal-based authentication
* In the future, we anticipate ACP itself supporting *some* form of
terminal-based authentication, but that hasn't been designed yet or gone
through the RFD process
* For now, this unblocks terminal-based auth

Release Notes:

- Added experimental terminal-based authentication to ACP support
2025-11-04 21:40:35 -05:00
Richard Feldman
60ee0dd19b Use our node runtime for ACP extensions (#41955)
Release Notes:

- Now ACP extensions use Zed's managed Node.js runtime

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-04 21:40:23 -05:00
Conrad Irwin
9fc4abd8de Re-enable preview auto-release (#41952)
Patch releases to preview will now automatically release

Release Notes:

- N/A
2025-11-04 19:26:33 -07:00
John Tur
2ead8c42fb Improve Windows text input for international keyboard layouts and IMEs (#41259)
- Custom handling of dead keys has been removed. UX for dead keys is now
the same as other applications on Windows.
- We could bring back some kind of custom UI, but only if UX is fully
compatible with expected Windows behavior (e.g. ability to move the
cursor after typing a dead key).
  - Fixes https://github.com/zed-industries/zed/issues/38838
- Character input via AltGr shift state now always has priority over
keybindings. This applies regardless of whether the keystroke used the
AltGr key or Ctrl+Alt to enter the shift state.
- In particular, we use the following heuristic to determine whether a
keystroke should trigger character input first or trigger keybindings
first:
- If the keystroke does not have any of Ctrl/Alt/Win down, trigger
keybindings first.
- Otherwise, determine the character that would be entered by the
keystroke. If it is a control character, or no character at all, trigger
keybindings first.
- Otherwise, the keystroke has _any_ of Ctrl/Alt/Win down and generates
a printable character. Compare this character against the character that
would be generated if the keystroke had _none_ of Ctrl/Alt/Win down:
- If the character is the same, the modifiers are not significant;
trigger keybindings first.
- If there is no active input handler, or the active input handler
indicates that it isn't accepting text input (e.g. when an operator is
pending in Vim mode), character entry is not useful; trigger keybindings
first.
- Otherwise, assume the modifiers enable access to an otherwise
difficult-to-enter key; trigger character entry first.
  - Fixes https://github.com/zed-industries/zed/issues/35862
- Fixes
https://github.com/zed-industries/zed/issues/40054#issuecomment-3447833349
  - Fixes https://github.com/zed-industries/zed/issues/41486
- TranslateMessage calls are no longer skipped for unhandled keystrokes.
This fixes language input keys on Japanese and Korean keyboards (and
surely other cases as well).
- To avoid any other missing-TranslateMessage headaches in the future,
the message loop has been rewritten in a "traditional" Win32 style,
where accelerators are handled in the message loop and TranslateMessage
is called in the intended manner.
  - Fixes https://github.com/zed-industries/zed/issues/39971
  - Fixes https://github.com/zed-industries/zed/issues/40300
  - Fixes https://github.com/zed-industries/zed/issues/40321
  - Fixes https://github.com/zed-industries/zed/issues/40335
  - Fixes https://github.com/zed-industries/zed/issues/40592
  - Fixes https://github.com/zed-industries/zed/issues/40638
- As a bonus, Alt+Space now opens the system menu, since it is triggered
by the WM_SYSCHAR generated by TranslateMessage.
- VK_PROCESSKEYs are now ignored rather than being unwrapped and matched
against keybindings. This ensures that IMEs will reliably receieve
keystrokes that they express interest in. This matches the behavior of
native Windows applications.
  - Fixes https://github.com/zed-industries/zed/issues/36736
  - Fixes https://github.com/zed-industries/zed/issues/39608
  - Fixes https://github.com/zed-industries/zed/issues/39991
  - Fixes https://github.com/zed-industries/zed/issues/41223
  - Fixes https://github.com/zed-industries/zed/issues/41656
  - Fixes https://github.com/zed-industries/zed/issues/34180
  - Fixes https://github.com/zed-industries/zed/issues/41766


Release Notes:

- windows: Improved keyboard input handling for international keyboard
layouts and IMEs
2025-11-04 20:28:12 -05:00
Danilo Leal
0a4b1ac696 inline assistant: Mention ability to add context with @ in the placeholder (#41950)
This has been possible in the inline assistant for ages now and maybe
you didn't know because we didn't say anything about it! This PR fixes
that by including that you can @-mention context on it the same you can
in the agent panel.

Release Notes:

- N/A
2025-11-04 21:18:49 -03:00
Conrad Irwin
f9fb855990 Fetch (just) enough refs in script/cherry-pick (#41949)
Before this change we'd download all the tagged commits, but none of
their ancestors,
this was slow and made cherry-picking fail.

Release Notes:

- N/A
2025-11-04 17:09:43 -07:00
Conrad Irwin
b587a62ac3 No-op commit to test cherry-picking (#41948)
Closes #ISSUE

Release Notes:

- N/A
2025-11-04 23:35:19 +00:00
Conrad Irwin
1b2e38bb33 More tweaks to CI pipeline (#41941)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-04 16:28:29 -07:00
Kirill Bulatov
4339c772e4 Tidy up Edit in json footer entries (#41890)
Before:
<img width="350" height="109" alt="before"
src="https://github.com/user-attachments/assets/d5d3e6bd-3a65-4d7d-8585-1e4d8f72997f"
/>

After:
<img width="310" height="103" alt="after"
src="https://github.com/user-attachments/assets/40137084-7323-4a79-b95b-a020c418646b"
/>

* All items got a keybinding label
* All items were made of a non-small size, to match the text size in the
right button
* Keybindings are rendered as disabled for disabled buttons

Release Notes:

- N/A

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-05 00:49:47 +02:00
tidely
ba7ea71c00 node_runtime: Improve proxy mapping (#41807)
Closes #ISSUE

More accurately map localhost to `127.0.0.1`. Previously we would
lowercase and simply replace all instances of localhost inside of the
URL string, meaning query parameters, username, password etc. could not
contain the string `localhost` or contain uppercase letters without
getting modified. Added a test ensuring the mapping logic works. The
previous implementation would fail this new test.

Release Notes:

- Improved the behavior of mapping `localhost` to `127.0.0.1` when
passing configured proxy urls to `node`
2025-11-04 17:11:54 -05:00
51 changed files with 923 additions and 474 deletions

View File

@@ -12,6 +12,10 @@ on:
description: branch
required: true
type: string
channel:
description: channel
required: true
type: string
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404
@@ -20,6 +24,16 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: get-app-token
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: cherry_pick::run_cherry_pick::cherry_pick
run: ./scripts/cherry-pick ${{ inputs.branch }} ${{ inputs.commit }}
run: ./script/cherry-pick ${{ inputs.branch }} ${{ inputs.commit }} ${{ inputs.channel }}
shell: bash -euxo pipefail {0}
env:
GIT_COMMITTER_NAME: Zed Zippy
GIT_COMMITTER_EMAIL: hi@zed.dev
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}

View File

@@ -12,6 +12,10 @@ on:
description: base
required: true
type: string
crate_name:
description: crate_name
type: string
default: ''
jobs:
run_perf:
runs-on: namespace-profile-16x32-ubuntu-2204
@@ -38,13 +42,25 @@ jobs:
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::cargo_perf_test
run: cargo perf-test -p gpui -- --json=${{ inputs.base }}
run: |2-
if [ -n "${{ inputs.crate_name }}" ]; then
cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.base }};
else
cargo perf-test -p vim -- --json=${{ inputs.base }};
fi
shell: bash -euxo pipefail {0}
- name: steps::git_checkout
run: git fetch origin ${{ inputs.head }} && git checkout ${{ inputs.head }}
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::cargo_perf_test
run: cargo perf-test -p gpui -- --json=${{ inputs.head }}
run: |2-
if [ -n "${{ inputs.crate_name }}" ]; then
cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.head }};
else
cargo perf-test -p vim -- --json=${{ inputs.head }};
fi
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::compare_runs
run: cargo perf-compare --save=results.md ${{ inputs.base }} ${{ inputs.head }}

View File

@@ -467,10 +467,7 @@ jobs:
auto_release_preview:
needs:
- upload_release_assets
if: |
false
&& startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false

View File

@@ -839,7 +839,7 @@ ui_input = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
debug = "limited"
debug = "full"
lto = "thin"
codegen-units = 1

View File

@@ -178,6 +178,7 @@ impl AcpConnection {
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
"terminal-auth": true,
})),
},
client_info: Some(acp::Implementation {

View File

@@ -1473,6 +1473,106 @@ impl AcpThreadView {
return;
};
// Check for the experimental "terminal-auth" _meta field
let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
if let Some(auth_method) = auth_method {
if let Some(meta) = &auth_method.meta {
if let Some(terminal_auth) = meta.get("terminal-auth") {
// Extract terminal auth details from meta
if let (Some(command), Some(label)) = (
terminal_auth.get("command").and_then(|v| v.as_str()),
terminal_auth.get("label").and_then(|v| v.as_str()),
) {
let args = terminal_auth
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let env = terminal_auth
.get("env")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| {
v.as_str().map(|val| (k.clone(), val.to_string()))
})
.collect::<HashMap<String, String>>()
})
.unwrap_or_default();
// Build SpawnInTerminal from _meta
let login = task::SpawnInTerminal {
id: task::TaskId(format!("external-agent-{}-login", label)),
full_label: label.to_string(),
label: label.to_string(),
command: Some(command.to_string()),
args,
command_label: label.to_string(),
env,
use_new_terminal: true,
allow_concurrent_runs: true,
hide: task::HideStrategy::Always,
..Default::default()
};
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
if let Some(workspace) = self.workspace.upgrade() {
let authenticate = Self::spawn_external_agent_login(
login, workspace, false, window, cx,
);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
let agent = self.agent.clone();
async move |this, cx| {
let result = authenticate.await;
match &result {
Ok(_) => telemetry::event!(
"Authenticate Agent Succeeded",
agent = agent.telemetry_id()
),
Err(_) => {
telemetry::event!(
"Authenticate Agent Failed",
agent = agent.telemetry_id(),
)
}
}
this.update_in(cx, |this, window, cx| {
if let Err(err) = result {
if let ThreadState::Unauthenticated {
pending_auth_method,
..
} = &mut this.thread_state
{
pending_auth_method.take();
}
this.handle_thread_error(err, cx);
} else {
this.reset(window, cx);
}
this.auth_task.take()
})
.ok();
}
}));
}
return;
}
}
}
}
if method.0.as_ref() == "gemini-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
@@ -1951,6 +2051,15 @@ impl AcpThreadView {
.into_any(),
};
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
)
} else {
false
};
let Some(thread) = self.thread() else {
return primary;
};
@@ -1959,7 +2068,13 @@ impl AcpThreadView {
v_flex()
.w_full()
.child(primary)
.child(self.render_thread_controls(&thread, cx))
.map(|this| {
if needs_confirmation {
this.child(self.render_generating(true))
} else {
this.child(self.render_thread_controls(&thread, cx))
}
})
.when_some(
self.thread_feedback.comments_editor.clone(),
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
@@ -4729,6 +4844,31 @@ impl AcpThreadView {
}
}
fn render_generating(&self, confirmation: bool) -> impl IntoElement {
h_flex()
.id("generating-spinner")
.py_2()
.px(rems_from_px(22.))
.map(|this| {
if confirmation {
this.gap_2()
.child(
h_flex()
.w_2()
.child(SpinnerLabel::sand().size(LabelSize::Small)),
)
.child(
LoadingLabel::new("Waiting Confirmation")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(SpinnerLabel::new().size(LabelSize::Small))
}
})
.into_any_element()
}
fn render_thread_controls(
&self,
thread: &Entity<AcpThread>,
@@ -4736,12 +4876,7 @@ impl AcpThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
return h_flex().id("thread-controls-container").child(
div()
.py_2()
.px(rems_from_px(22.))
.child(SpinnerLabel::new().size(LabelSize::Small)),
);
return self.render_generating(false).into_any_element();
}
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
@@ -4829,7 +4964,10 @@ impl AcpThreadView {
);
}
container.child(open_as_markdown).child(scroll_to_top)
container
.child(open_as_markdown)
.child(scroll_to_top)
.into_any_element()
}
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {

View File

@@ -1013,7 +1013,7 @@ impl AgentConfiguration {
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi),
"Codex",
"Codex CLI",
false,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))

View File

@@ -1880,7 +1880,12 @@ impl AgentPanel {
{
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in("New…", &ToggleNewThreadMenu, &focus_handle, cx)
Tooltip::for_action_in(
"New Thread…",
&ToggleNewThreadMenu,
&focus_handle,
cx,
)
}
},
)
@@ -1978,7 +1983,7 @@ impl AgentPanel {
.separator()
.header("External Agents")
.item(
ContextMenuEntry::new("New Claude Code Thread")
ContextMenuEntry::new("New Claude Code")
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2004,7 +2009,7 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Codex Thread")
ContextMenuEntry::new("New Codex CLI")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2030,7 +2035,7 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Gemini CLI Thread")
ContextMenuEntry::new("New Gemini CLI")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_via_collab)
@@ -2074,7 +2079,7 @@ impl AgentPanel {
for agent_name in agent_names {
let icon_path = agent_server_store_read.agent_icon(&agent_name);
let mut entry =
ContextMenuEntry::new(format!("New {} Thread", agent_name));
ContextMenuEntry::new(format!("New {}", agent_name));
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_path(icon_path);
} else {

View File

@@ -260,10 +260,10 @@ impl<T: 'static> PromptEditor<T> {
let agent_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} to chat"))
.map(|keybinding| format!("{keybinding} to chat"))
.unwrap_or_default();
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
format!("{action}… ({agent_panel_keybinding}↓↑ for history — @ to include context)")
}
pub fn prompt(&self, cx: &App) -> String {

View File

@@ -697,6 +697,7 @@ impl Render for NewProcessModal {
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant);
let secondary_action = menu::SecondaryConfirm.boxed_clone();
match self.mode {
NewProcessMode::Launch => el.child(
container
@@ -706,6 +707,7 @@ impl Render for NewProcessModal {
.on_click(cx.listener(|this, _, window, cx| {
this.save_debug_scenario(window, cx);
}))
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.disabled(
self.debugger.is_none()
|| self
@@ -749,7 +751,6 @@ impl Render for NewProcessModal {
container
.child(div().child({
Button::new("edit-attach-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(secondary_action.boxed_clone(), cx)
@@ -1192,7 +1193,7 @@ impl PickerDelegate for DebugDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"Find a debug task, or debug a command.".into()
"Find a debug task, or debug a command".into()
}
fn update_matches(
@@ -1453,18 +1454,17 @@ impl PickerDelegate for DebugDelegate {
.child({
let action = menu::SecondaryConfirm.boxed_clone();
if self.matches.is_empty() {
Button::new("edit-debug-json", "Edit debug.json")
.label_size(LabelSize::Small)
.on_click(cx.listener(|_picker, _, window, cx| {
Button::new("edit-debug-json", "Edit debug.json").on_click(cx.listener(
|_picker, _, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
}))
},
))
} else {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)

View File

@@ -7599,18 +7599,17 @@ impl Editor {
)
}
fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
if invert {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
}
} else {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
fn is_cmd_or_ctrl_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
match EditorSettings::get_global(cx).multi_cursor_modifier {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
}
fn is_alt_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
match EditorSettings::get_global(cx).multi_cursor_modifier {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
}
}
@@ -7619,9 +7618,9 @@ impl Editor {
cx: &mut Context<Self>,
) -> Option<ColumnarMode> {
if modifiers.shift && modifiers.number_of_modifiers() == 2 {
if Self::multi_cursor_modifier(false, modifiers, cx) {
if Self::is_cmd_or_ctrl_pressed(modifiers, cx) {
Some(ColumnarMode::FromMouse)
} else if Self::multi_cursor_modifier(true, modifiers, cx) {
} else if Self::is_alt_pressed(modifiers, cx) {
Some(ColumnarMode::FromSelection)
} else {
None
@@ -24194,6 +24193,10 @@ impl EntityInputHandler for Editor {
let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot());
Some(utf16_offset.0)
}
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
self.input_enabled
}
}
trait SelectionExt {

View File

@@ -820,7 +820,7 @@ impl EditorElement {
editor.select(
SelectPhase::Begin {
position,
add: Editor::multi_cursor_modifier(true, &modifiers, cx),
add: Editor::is_alt_pressed(&modifiers, cx),
click_count,
},
window,
@@ -1004,7 +1004,7 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx);
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx);
if let Some(mouse_position) = event.mouse_position()
&& !pending_nonempty_selections

View File

@@ -116,7 +116,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);
if !hovered_link_modifier || self.has_pending_selection() {
self.hide_hovered_link(cx);
return;
@@ -241,8 +241,8 @@ impl Editor {
}
})
.collect();
let navigate_task =
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
let split = Self::is_alt_pressed(&modifiers, cx);
let navigate_task = self.navigate_to_hover_links(None, links, split, window, cx);
self.select(SelectPhase::End, window, cx);
return navigate_task;
}
@@ -261,7 +261,8 @@ impl Editor {
);
let navigate_task = if point.as_valid().is_some() {
match (modifiers.shift, modifiers.alt) {
let split = Self::is_alt_pressed(&modifiers, cx);
match (modifiers.shift, split) {
(true, true) => {
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
}

View File

@@ -164,6 +164,15 @@ pub struct AgentServerManifestEntry {
/// args = ["--serve"]
/// sha256 = "abc123..." # optional
/// ```
///
/// For Node.js-based agents, you can use "node" as the cmd to automatically
/// use Zed's managed Node.js runtime instead of relying on the user's PATH:
/// ```toml
/// [agent_servers.nodeagent.targets.darwin-aarch64]
/// archive = "https://example.com/nodeagent.zip"
/// cmd = "node"
/// args = ["index.js", "--port", "3000"]
/// ```
pub targets: HashMap<String, TargetConfig>,
}

View File

@@ -32,7 +32,7 @@ use project::{
},
};
use settings::{Settings, SettingsStore};
use std::any::{Any, TypeId};
use std::{any::{Any, TypeId}, time::Duration};
use std::ops::Range;
use std::sync::Arc;
use theme::ActiveTheme;
@@ -590,6 +590,10 @@ impl ProjectDiff {
.ok();
})?;
}
cx.background_executor().timer(Duration::from_millis(5)).await;
this.update(cx, |_, cx| {
cx.notify();
})?;
}
this.update(cx, |this, cx| {
this.pending_scroll.take();

View File

@@ -70,6 +70,11 @@ pub trait EntityInputHandler: 'static + Sized {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize>;
/// See [`InputHandler::accepts_text_input`] for details
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
true
}
}
/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
@@ -177,4 +182,9 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
view.character_index_for_point(point, window, cx)
})
}
fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.view
.update(cx, |view, cx| view.accepts_text_input(window, cx))
}
}

View File

@@ -25,6 +25,10 @@ pub struct KeyDownEvent {
/// Whether the key is currently held down.
pub is_held: bool,
/// Whether to prefer character input over keybindings for this keystroke.
/// In some cases, like AltGr on Windows, modifiers are significant for character input.
pub prefer_character_input: bool,
}
impl Sealed for KeyDownEvent {}

View File

@@ -1012,6 +1012,11 @@ impl PlatformInputHandler {
.ok()
.flatten()
}
#[allow(dead_code)]
pub(crate) fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.handler.accepts_text_input(window, cx)
}
}
/// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -1120,6 +1125,11 @@ pub trait InputHandler: 'static {
fn apple_press_and_hold_enabled(&mut self) -> bool {
true
}
/// Returns whether this handler is accepting text input to be inserted.
fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
true
}
}
/// The variables that can be configured when creating a new window

View File

@@ -1382,6 +1382,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let input = PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
prefer_character_input: false,
});
state.repeat.current_id += 1;
@@ -1395,6 +1396,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let input = PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: true,
prefer_character_input: false,
});
move |_event, _metadata, this| {
let mut client = this.get_client();
@@ -1479,6 +1481,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
key_char: Some(commit_text),
},
is_held: false,
prefer_character_input: false,
}));
} else {
window.handle_ime(ImeInput::InsertText(commit_text));

View File

@@ -1047,6 +1047,7 @@ impl X11Client {
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
prefer_character_input: false,
}));
}
Event::KeyRelease(event) => {

View File

@@ -131,6 +131,7 @@ impl PlatformInput {
NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
keystroke: parse_keystroke(native_event),
is_held: native_event.isARepeat() == YES,
prefer_character_input: false,
})),
NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
keystroke: parse_keystroke(native_event),

View File

@@ -2318,6 +2318,7 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: false,
prefer_character_input: false,
}));
state.as_ref().lock().do_command_handled = Some(!handled.propagate);
}

View File

@@ -26,6 +26,7 @@ pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6;
pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7;
pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8;
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
@@ -92,13 +93,10 @@ impl WindowsWindowInner {
}
WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam),
WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam),
WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam),
WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam),
WM_SYSCOMMAND => self.handle_system_command(wparam),
WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam),
WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam),
WM_SYSKEYUP => self.handle_syskeyup_msg(wparam, lparam),
WM_KEYUP => self.handle_keyup_msg(wparam, lparam),
WM_GPUI_KEYDOWN => self.handle_keydown_msg(wparam, lparam),
WM_CHAR => self.handle_char_msg(wparam),
WM_DEADCHAR => self.handle_dead_char_msg(wparam),
WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle),
WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam),
WM_SETCURSOR => self.handle_set_cursor(handle, lparam),
@@ -327,35 +325,9 @@ impl WindowsWindowInner {
Some(0)
}
fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
fn handle_syskeyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
let mut lock = self.state.borrow_mut();
let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
})
})?;
let mut func = lock.callbacks.input.take()?;
drop(lock);
let handled = !func(input).propagate;
let mut lock = self.state.borrow_mut();
lock.callbacks.input = Some(func);
if handled {
lock.system_key_handled = true;
Some(0)
} else {
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
// shortcuts.
None
}
}
fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
let mut lock = self.state.borrow_mut();
let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let input = handle_key_event(wparam, lparam, &mut lock, |keystroke, _| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
})?;
let mut func = lock.callbacks.input.take()?;
@@ -369,27 +341,24 @@ impl WindowsWindowInner {
// It's a known bug that you can't trigger `ctrl-shift-0`. See:
// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers
fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
let mut lock = self.state.borrow_mut();
let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
})
}) else {
let Some(input) = handle_key_event(
wparam,
lparam,
&mut lock,
|keystroke, prefer_character_input| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
prefer_character_input,
})
},
) else {
return Some(1);
};
drop(lock);
let is_composing = self
.with_input_handler(|input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
if is_composing {
translate_message(handle, wparam, lparam);
return Some(0);
}
let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else {
return Some(1);
};
@@ -398,17 +367,12 @@ impl WindowsWindowInner {
self.state.borrow_mut().callbacks.input = Some(func);
if handled {
Some(0)
} else {
translate_message(handle, wparam, lparam);
Some(1)
}
if handled { Some(0) } else { Some(1) }
}
fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
fn handle_keyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
let mut lock = self.state.borrow_mut();
let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke, _| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
}) else {
return Some(1);
@@ -434,14 +398,6 @@ impl WindowsWindowInner {
Some(0)
}
fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option<isize> {
let ch = char::from_u32(wparam.0 as u32)?.to_string();
self.with_input_handler(|input_handler| {
input_handler.replace_and_mark_text_in_range(None, &ch, None);
});
None
}
fn handle_mouse_down_msg(
&self,
handle: HWND,
@@ -1127,17 +1083,6 @@ impl WindowsWindowInner {
Some(0)
}
fn handle_system_command(&self, wparam: WPARAM) -> Option<isize> {
if wparam.0 == SC_KEYMENU as usize {
let mut lock = self.state.borrow_mut();
if lock.system_key_handled {
lock.system_key_handled = false;
return Some(0);
}
}
None
}
fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option<isize> {
// lParam is a pointer to a string that indicates the area containing the system parameter
// that was changed.
@@ -1281,30 +1226,14 @@ impl WindowsWindowInner {
}
}
#[inline]
fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
let msg = MSG {
hwnd: handle,
message: WM_KEYDOWN,
wParam: wparam,
lParam: lparam,
// It seems like leaving the following two parameters empty doesn't break key events, they still work as expected.
// But if any bugs pop up after this PR, this is probably the place to look first.
time: 0,
pt: POINT::default(),
};
unsafe { TranslateMessage(&msg).ok().log_err() };
}
fn handle_key_event<F>(
handle: HWND,
wparam: WPARAM,
lparam: LPARAM,
state: &mut WindowsWindowState,
f: F,
) -> Option<PlatformInput>
where
F: FnOnce(Keystroke) -> PlatformInput,
F: FnOnce(Keystroke, bool) -> PlatformInput,
{
let virtual_key = VIRTUAL_KEY(wparam.loword());
let modifiers = current_modifiers();
@@ -1323,10 +1252,7 @@ where
capslock: current_capslock(),
}))
}
VK_PACKET => {
translate_message(handle, wparam, lparam);
None
}
VK_PACKET => None,
VK_CAPITAL => {
let capslock = current_capslock();
if state
@@ -1342,13 +1268,8 @@ where
}))
}
vkey => {
let vkey = if vkey == VK_PROCESSKEY {
VIRTUAL_KEY(unsafe { ImmGetVirtualKey(handle) } as u16)
} else {
vkey
};
let keystroke = parse_normal_key(vkey, lparam, modifiers)?;
Some(f(keystroke))
Some(f(keystroke.0, keystroke.1))
}
}
}
@@ -1408,7 +1329,7 @@ fn parse_normal_key(
vkey: VIRTUAL_KEY,
lparam: LPARAM,
mut modifiers: Modifiers,
) -> Option<Keystroke> {
) -> Option<(Keystroke, bool)> {
let mut key_char = None;
let key = parse_immutable(vkey).or_else(|| {
let scan_code = lparam.hiword() & 0xFF;
@@ -1421,11 +1342,88 @@ fn parse_normal_key(
);
get_keystroke_key(vkey, scan_code as u32, &mut modifiers)
})?;
Some(Keystroke {
modifiers,
key,
key_char,
})
let prefer_character_input = should_prefer_character_input(vkey, lparam.hiword() & 0xFF);
Some((
Keystroke {
modifiers,
key,
key_char,
},
prefer_character_input,
))
}
fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
let mut keyboard_state = [0u8; 256];
unsafe {
if GetKeyboardState(&mut keyboard_state).is_err() {
return false;
}
}
let mut buffer_c = [0u16; 8];
let result_c = unsafe {
ToUnicode(
vkey.0 as u32,
scan_code as u32,
Some(&keyboard_state),
&mut buffer_c,
0x4,
)
};
if result_c < 0 {
return false;
}
let c = &buffer_c[..result_c as usize];
if char::decode_utf16(c.iter().copied())
.next()
.and_then(|ch| ch.ok())
.map(|ch| ch.is_control())
.unwrap_or(true)
{
return false;
}
// Workaround for some bug that makes the compiler think keyboard_state is still zeroed out
let keyboard_state = std::hint::black_box(keyboard_state);
let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0;
let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0;
let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0
|| (keyboard_state[VK_RWIN.0 as usize] & 0x80) != 0;
let has_modifiers = ctrl_down || alt_down || win_down;
if !has_modifiers {
return false;
}
let mut state_no_modifiers = keyboard_state;
state_no_modifiers[VK_CONTROL.0 as usize] = 0;
state_no_modifiers[VK_LCONTROL.0 as usize] = 0;
state_no_modifiers[VK_RCONTROL.0 as usize] = 0;
state_no_modifiers[VK_MENU.0 as usize] = 0;
state_no_modifiers[VK_LMENU.0 as usize] = 0;
state_no_modifiers[VK_RMENU.0 as usize] = 0;
state_no_modifiers[VK_LWIN.0 as usize] = 0;
state_no_modifiers[VK_RWIN.0 as usize] = 0;
let mut buffer_c_no_modifiers = [0u16; 8];
let result_c_no_modifiers = unsafe {
ToUnicode(
vkey.0 as u32,
scan_code as u32,
Some(&state_no_modifiers),
&mut buffer_c_no_modifiers,
0x4,
)
};
if result_c_no_modifiers <= 0 {
return false;
}
let c_no_modifiers = &buffer_c_no_modifiers[..result_c_no_modifiers as usize];
c != c_no_modifiers
}
fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option<String> {
@@ -1460,25 +1458,11 @@ fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
unsafe { GetKeyState(vkey.0 as i32) < 0 }
}
fn keyboard_uses_altgr() -> bool {
use crate::platform::windows::keyboard::WindowsKeyboardLayout;
WindowsKeyboardLayout::new()
.map(|layout| layout.uses_altgr())
.unwrap_or(false)
}
#[inline]
pub(crate) fn current_modifiers() -> Modifiers {
let lmenu_pressed = is_virtual_key_pressed(VK_LMENU);
let rmenu_pressed = is_virtual_key_pressed(VK_RMENU);
let lcontrol_pressed = is_virtual_key_pressed(VK_LCONTROL);
// Only treat right Alt + left Ctrl as AltGr on keyboards that actually use it
let altgr = keyboard_uses_altgr() && rmenu_pressed && lcontrol_pressed;
Modifiers {
control: is_virtual_key_pressed(VK_CONTROL) && !altgr,
alt: (lmenu_pressed || rmenu_pressed) && !altgr,
control: is_virtual_key_pressed(VK_CONTROL),
alt: is_virtual_key_pressed(VK_MENU),
shift: is_virtual_key_pressed(VK_SHIFT),
platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN),
function: false,

View File

@@ -108,39 +108,6 @@ impl WindowsKeyboardLayout {
name: "unknown".to_string(),
}
}
pub(crate) fn uses_altgr(&self) -> bool {
// Check if this is a known AltGr layout by examining the layout ID
// The layout ID is a hex string like "00000409" (US) or "00000407" (German)
// Extract the language ID (last 4 bytes)
let id_bytes = self.id.as_bytes();
if id_bytes.len() >= 4 {
let lang_id = &id_bytes[id_bytes.len() - 4..];
// List of keyboard layouts that use AltGr (non-exhaustive)
matches!(
lang_id,
b"0407" | // German
b"040C" | // French
b"040A" | // Spanish
b"0415" | // Polish
b"0413" | // Dutch
b"0816" | // Portuguese
b"041D" | // Swedish
b"0414" | // Norwegian
b"040B" | // Finnish
b"041F" | // Turkish
b"0419" | // Russian
b"0405" | // Czech
b"040E" | // Hungarian
b"0424" | // Slovenian
b"041A" | // Croatian
b"041B" | // Slovak
b"0418" // Romanian
)
} else {
false
}
}
}
impl WindowsKeyboardMapper {
@@ -258,7 +225,7 @@ pub(crate) fn generate_key_char(
}
let mut buffer = [0; 8];
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) };
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) };
match len {
len if len > 0 => String::from_utf16(&buffer[..len as usize])

View File

@@ -272,6 +272,22 @@ impl WindowsPlatform {
}
}
fn translate_accelerator(msg: &MSG) -> Option<()> {
if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN {
return None;
}
let result = unsafe {
SendMessageW(
msg.hwnd,
WM_GPUI_KEYDOWN,
Some(msg.wParam),
Some(msg.lParam),
)
};
(result.0 == 0).then_some(())
}
impl Platform for WindowsPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
@@ -312,7 +328,10 @@ impl Platform for WindowsPlatform {
let mut msg = MSG::default();
unsafe {
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
DispatchMessageW(&msg);
if translate_accelerator(&msg).is_none() {
_ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}

View File

@@ -45,7 +45,6 @@ pub struct WindowsWindowState {
pub pending_surrogate: Option<u16>,
pub last_reported_modifiers: Option<Modifiers>,
pub last_reported_capslock: Option<Capslock>,
pub system_key_handled: bool,
pub hovered: bool,
pub renderer: DirectXRenderer,
@@ -112,7 +111,6 @@ impl WindowsWindowState {
let pending_surrogate = None;
let last_reported_modifiers = None;
let last_reported_capslock = None;
let system_key_handled = false;
let hovered = false;
let click_state = ClickState::new();
let nc_button_pressed = None;
@@ -133,7 +131,6 @@ impl WindowsWindowState {
pending_surrogate,
last_reported_modifiers,
last_reported_capslock,
system_key_handled,
hovered,
renderer,
click_state,

View File

@@ -3558,6 +3558,7 @@ impl Window {
PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
prefer_character_input: false,
}),
cx,
);
@@ -3856,17 +3857,35 @@ impl Window {
return;
}
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {
self.dispatch_keystroke_observers(
event,
Some(binding.action),
match_result.context_stack,
cx,
);
self.pending_input_changed(cx);
return;
let skip_bindings = event
.downcast_ref::<KeyDownEvent>()
.filter(|key_down_event| key_down_event.prefer_character_input)
.map(|_| {
self.platform_window
.take_input_handler()
.map_or(false, |mut input_handler| {
let accepts = input_handler.accepts_text_input(self, cx);
self.platform_window.set_input_handler(input_handler);
// If modifiers are not excessive (e.g. AltGr), and the input handler is accepting text input,
// we prefer the text input over bindings.
accepts
})
})
.unwrap_or(false);
if !skip_bindings {
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
if !cx.propagate_event {
self.dispatch_keystroke_observers(
event,
Some(binding.action),
match_result.context_stack,
cx,
);
self.pending_input_changed(cx);
return;
}
}
}
@@ -3975,6 +3994,7 @@ impl Window {
let event = KeyDownEvent {
keystroke: replay.keystroke.clone(),
is_held: false,
prefer_character_input: true,
};
cx.propagate_event = true;

View File

@@ -16,7 +16,7 @@ use parking_lot::Mutex;
#[cfg(feature = "test-support")]
use std::fmt;
use std::{any::type_name, sync::Arc};
pub use url::Url;
pub use url::{Host, Url};
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum RedirectPolicy {

View File

@@ -969,6 +969,7 @@ impl Buffer {
/// Builds a [`Buffer`] with the given underlying [`TextBuffer`], diff base, [`File`] and [`Capability`].
pub fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>, capability: Capability) -> Self {
log::info!("file: {:?}", file.as_ref().map(|f| f.path()));
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
let snapshot = buffer.snapshot();
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
@@ -3366,7 +3367,19 @@ impl BufferSnapshot {
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
let offset = position.to_offset(self);
self.syntax_layers_for_range(offset..offset, false)
.filter(|l| l.node().end_byte() > offset)
.filter(|l| {
if let Some(ranges) = l.included_sub_ranges {
ranges.iter().any(|range| {
let start = range.start.to_offset(self);
start <= offset && {
let end = range.end.to_offset(self);
offset < end
}
})
} else {
l.node().start_byte() <= offset && l.node().end_byte() > offset
}
})
.last()
}

View File

@@ -2633,7 +2633,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut App) {
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("ERB")
.language_for_name("HTML+ERB")
.now_or_never()
.unwrap()
.ok(),
@@ -2753,6 +2753,50 @@ fn test_language_at_for_markdown_code_block(cx: &mut App) {
});
}
#[gpui::test]
fn test_syntax_layer_at_for_injected_languages(cx: &mut App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let text = r#"
```html+erb
<div>Hello</div>
<%= link_to "Some", "https://zed.dev" %>
```
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(erb_lang()));
language_registry.add(Arc::new(html_lang()));
language_registry.add(Arc::new(ruby_lang()));
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
language_registry
.language_for_name("HTML+ERB")
.now_or_never()
.unwrap()
.ok(),
cx,
);
let snapshot = buffer.snapshot();
// Test points in the code line
let html_point = Point::new(1, 4);
let language = snapshot.language_at(html_point).unwrap();
assert_eq!(language.name().as_ref(), "HTML");
let ruby_point = Point::new(2, 6);
let language = snapshot.language_at(ruby_point).unwrap();
assert_eq!(language.name().as_ref(), "Ruby");
buffer
});
}
#[gpui::test]
fn test_serialization(cx: &mut gpui::App) {
let mut now = Instant::now();
@@ -3655,7 +3699,7 @@ fn html_lang() -> Language {
fn erb_lang() -> Language {
Language::new(
LanguageConfig {
name: "ERB".into(),
name: "HTML+ERB".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["erb".to_string()],
..Default::default()
@@ -3673,15 +3717,15 @@ fn erb_lang() -> Language {
.with_injection_query(
r#"
(
(code) @injection.content
(#set! injection.language "ruby")
(#set! injection.combined)
(code) @content
(#set! "language" "ruby")
(#set! "combined")
)
(
(content) @injection.content
(#set! injection.language "html")
(#set! injection.combined)
(content) @content
(#set! "language" "html")
(#set! "combined")
)
"#,
)

View File

@@ -587,6 +587,8 @@ impl SyntaxSnapshot {
let changed_ranges;
let mut included_ranges = step.included_ranges;
let is_combined = matches!(step.mode, ParseMode::Combined { .. });
for range in &mut included_ranges {
range.start_byte -= step_start_byte;
range.end_byte -= step_start_byte;
@@ -749,16 +751,20 @@ impl SyntaxSnapshot {
);
}
let included_sub_ranges: Option<Vec<Range<Anchor>>> =
(included_ranges.len() > 1).then_some(
let included_sub_ranges: Option<Vec<Range<Anchor>>> = if is_combined {
Some(
included_ranges
.into_iter()
.filter(|r| r.start_byte < r.end_byte)
.map(|r| {
text.anchor_before(r.start_byte + step_start_byte)
..text.anchor_after(r.end_byte + step_start_byte)
})
.collect(),
);
)
} else {
None
};
SyntaxLayerContent::Parsed {
tree,
language,

View File

@@ -538,27 +538,27 @@ impl OpenAiEventMapper {
return events;
};
if let Some(content) = choice.delta.content.clone() {
if !content.is_empty() {
if let Some(delta) = choice.delta.as_ref() {
if let Some(content) = delta.content.clone() {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
if let Some(tool_calls) = delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
if let Some(tool_id) = tool_call.id.clone() {
entry.id = tool_id;
}
if let Some(function) = tool_call.function.as_ref() {
if let Some(name) = function.name.clone() {
entry.name = name;
if let Some(tool_id) = tool_call.id.clone() {
entry.id = tool_id;
}
if let Some(arguments) = function.arguments.clone() {
entry.arguments.push_str(&arguments);
if let Some(function) = tool_call.function.as_ref() {
if let Some(name) = function.name.clone() {
entry.name = name;
}
if let Some(arguments) = function.arguments.clone() {
entry.arguments.push_str(&arguments);
}
}
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::{Context as _, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
use http_client::{HttpClient, Url};
use http_client::{Host, HttpClient, Url};
use log::Level;
use semver::Version;
use serde::Deserialize;
@@ -13,6 +13,7 @@ use std::{
env::{self, consts},
ffi::OsString,
io,
net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf},
process::Output,
sync::Arc,
@@ -799,17 +800,18 @@ fn configure_npm_command(
command.args(["--prefix".into(), directory.to_path_buf()]);
}
if let Some(proxy) = proxy {
if let Some(mut proxy) = proxy.cloned() {
// Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
// NodeRuntime without environment information can not parse `localhost`
// correctly.
// TODO: map to `[::1]` if we are using ipv6
let proxy = proxy
.to_string()
.to_ascii_lowercase()
.replace("localhost", "127.0.0.1");
if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost"))
{
// When localhost is a valid Host, so is `127.0.0.1`
let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST));
}
command.args(["--proxy", &proxy]);
command.args(["--proxy", proxy.as_str()]);
}
#[cfg(windows)]
@@ -830,3 +832,46 @@ fn configure_npm_command(
}
}
}
#[cfg(test)]
mod tests {
use http_client::Url;
use super::configure_npm_command;
// Map localhost to 127.0.0.1
// NodeRuntime without environment information can not parse `localhost` correctly.
#[test]
fn test_configure_npm_command_map_localhost_proxy() {
const CASES: [(&str, &str); 4] = [
// Map localhost to 127.0.0.1
("http://localhost:9090/", "http://127.0.0.1:9090/"),
("https://google.com/", "https://google.com/"),
(
"http://username:password@proxy.thing.com:8080/",
"http://username:password@proxy.thing.com:8080/",
),
// Test when localhost is contained within a different part of the URL
(
"http://username:localhost@localhost:8080/",
"http://username:localhost@127.0.0.1:8080/",
),
];
for (proxy, mapped_proxy) in CASES {
let mut dummy = smol::process::Command::new("");
let proxy = Url::parse(proxy).unwrap();
configure_npm_command(&mut dummy, None, Some(&proxy));
let proxy = dummy
.get_args()
.skip_while(|&arg| arg != "--proxy")
.skip(1)
.next();
let proxy = proxy.expect("Proxy was not passed to Command correctly");
assert_eq!(
proxy, mapped_proxy,
"Incorrectly mapped localhost to 127.0.0.1"
);
}
}
}

View File

@@ -420,7 +420,7 @@ pub struct Usage {
#[derive(Serialize, Deserialize, Debug)]
pub struct ChoiceDelta {
pub index: u32,
pub delta: ResponseMessageDelta,
pub delta: Option<ResponseMessageDelta>,
pub finish_reason: Option<String>,
}

View File

@@ -259,6 +259,7 @@ impl AgentServerStore {
// Insert agent servers from extension manifests
match &self.state {
AgentServerStoreState::Local {
node_runtime,
project_environment,
fs,
http_client,
@@ -289,6 +290,7 @@ impl AgentServerStore {
Box::new(LocalExtensionArchiveAgent {
fs: fs.clone(),
http_client: http_client.clone(),
node_runtime: node_runtime.clone(),
project_environment: project_environment.clone(),
extension_id: Arc::from(ext_id),
agent_id: agent_name.clone(),
@@ -1356,6 +1358,7 @@ fn asset_name(version: &str) -> Option<String> {
struct LocalExtensionArchiveAgent {
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
project_environment: Entity<ProjectEnvironment>,
extension_id: Arc<str>,
agent_id: Arc<str>,
@@ -1379,6 +1382,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
let fs = self.fs.clone();
let http_client = self.http_client.clone();
let node_runtime = self.node_runtime.clone();
let project_environment = self.project_environment.downgrade();
let extension_id = self.extension_id.clone();
let agent_id = self.agent_id.clone();
@@ -1526,23 +1530,29 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
// Validate and resolve cmd path
let cmd = &target_config.cmd;
if cmd.contains("..") {
anyhow::bail!("command path cannot contain '..': {}", cmd);
}
let cmd_path = if cmd.starts_with("./") || cmd.starts_with(".\\") {
// Relative to extraction directory
version_dir.join(&cmd[2..])
let cmd_path = if cmd == "node" {
// Use Zed's managed Node.js runtime
node_runtime.binary_path().await?
} else {
// On PATH
anyhow::bail!("command must be relative (start with './'): {}", cmd);
};
if cmd.contains("..") {
anyhow::bail!("command path cannot contain '..': {}", cmd);
}
anyhow::ensure!(
fs.is_file(&cmd_path).await,
"Missing command {} after extraction",
cmd_path.to_string_lossy()
);
if cmd.starts_with("./") || cmd.starts_with(".\\") {
// Relative to extraction directory
let cmd_path = version_dir.join(&cmd[2..]);
anyhow::ensure!(
fs.is_file(&cmd_path).await,
"Missing command {} after extraction",
cmd_path.to_string_lossy()
);
cmd_path
} else {
// On PATH
anyhow::bail!("command must be relative (start with './'): {}", cmd);
}
};
let command = AgentServerCommand {
path: cmd_path,
@@ -1828,6 +1838,7 @@ mod extension_agent_tests {
let agent = LocalExtensionArchiveAgent {
fs,
http_client,
node_runtime: node_runtime::NodeRuntime::unavailable(),
project_environment,
extension_id: Arc::from("my-extension"),
agent_id: Arc::from("my-agent"),
@@ -1893,6 +1904,48 @@ mod extension_agent_tests {
assert_eq!(target.cmd, "./release-agent");
}
#[gpui::test]
async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.background_executor.clone());
let http_client = http_client::FakeHttpClient::with_404_response();
let node_runtime = NodeRuntime::unavailable();
let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone()));
let project_environment = cx.new(|cx| {
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
});
let agent = LocalExtensionArchiveAgent {
fs: fs.clone(),
http_client,
node_runtime,
project_environment,
extension_id: Arc::from("node-extension"),
agent_id: Arc::from("node-agent"),
targets: {
let mut map = HashMap::default();
map.insert(
"darwin-aarch64".to_string(),
extension::TargetConfig {
archive: "https://example.com/node-agent.zip".into(),
cmd: "node".into(),
args: vec!["index.js".into()],
sha256: None,
},
);
map
},
env: HashMap::default(),
};
// Verify that when cmd is "node", it attempts to use the node runtime
assert_eq!(agent.extension_id.as_ref(), "node-extension");
assert_eq!(agent.agent_id.as_ref(), "node-agent");
let target = agent.targets.get("darwin-aarch64").unwrap();
assert_eq!(target.cmd, "node");
assert_eq!(target.args, vec!["index.js"]);
}
#[test]
fn test_tilde_expansion_in_settings() {
let settings = settings::BuiltinAgentServerSettings {

View File

@@ -319,6 +319,8 @@ impl BranchDiff {
});
}
});
// let names: Vec<_> = output.iter().map(|o| o.repo_path.as_unix_str()).collect();
// eprintln!("OUTPUT IS *********************************: {names:?}");
output
}

View File

@@ -12349,10 +12349,7 @@ impl LspStore {
.update(cx, |buffer, _| buffer.wait_for_version(version))?
.await?;
lsp_store.update(cx, |lsp_store, cx| {
let lsp_data = lsp_store
.lsp_data
.entry(buffer_id)
.or_insert_with(|| BufferLspData::new(&buffer, cx));
let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
let chunks_queried_for = lsp_data
.inlay_hints
.applicable_chunks(&[range])

View File

@@ -1070,14 +1070,21 @@ impl SshSocket {
}
async fn shell(&self) -> String {
let default_shell = "sh";
match self
.run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"])
.await
{
Ok(shell) => shell.trim().to_owned(),
Ok(shell) => match shell.trim() {
"" => {
log::error!("$SHELL is not set, falling back to {default_shell}");
default_shell.to_owned()
}
shell => shell.to_owned(),
},
Err(e) => {
log::error!("Failed to get shell: {e}");
"sh".to_owned()
default_shell.to_owned()
}
}
}

View File

@@ -285,6 +285,10 @@ impl Disableable for Button {
/// This results in a button that is disabled and does not respond to click events.
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
self.key_binding = self
.key_binding
.take()
.map(|binding| binding.disabled(disabled));
self
}
}

View File

@@ -1,5 +1,5 @@
use crate::prelude::*;
use gpui::{Animation, AnimationExt, FontWeight, pulsating_between};
use gpui::{Animation, AnimationExt, FontWeight};
use std::time::Duration;
#[derive(IntoElement)]
@@ -84,38 +84,29 @@ impl RenderOnce for LoadingLabel {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let text = self.text.clone();
self.base
.color(Color::Muted)
.with_animations(
"loading_label",
vec![
Animation::new(Duration::from_secs(1)),
Animation::new(Duration::from_secs(1)).repeat(),
],
move |mut label, animation_ix, delta| {
match animation_ix {
0 => {
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
let text = SharedString::from(text[0..chars_to_show].to_string());
label.set_text(text);
}
1 => match delta {
d if d < 0.25 => label.set_text(text.clone()),
d if d < 0.5 => label.set_text(format!("{}.", text)),
d if d < 0.75 => label.set_text(format!("{}..", text)),
_ => label.set_text(format!("{}...", text)),
},
_ => {}
self.base.color(Color::Muted).with_animations(
"loading_label",
vec![
Animation::new(Duration::from_secs(1)),
Animation::new(Duration::from_secs(1)).repeat(),
],
move |mut label, animation_ix, delta| {
match animation_ix {
0 => {
let chars_to_show = (delta * text.len() as f32).ceil() as usize;
let text = SharedString::from(text[0..chars_to_show].to_string());
label.set_text(text);
}
label
},
)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.map_element(|label| label.alpha(delta)),
)
1 => match delta {
d if d < 0.25 => label.set_text(text.clone()),
d if d < 0.5 => label.set_text(format!("{}.", text)),
d if d < 0.75 => label.set_text(format!("{}..", text)),
_ => label.set_text(format!("{}...", text)),
},
_ => {}
}
label
},
)
}
}

View File

@@ -8,6 +8,7 @@ pub enum SpinnerVariant {
#[default]
Dots,
DotsVariant,
Sand,
}
/// A spinner indication, based on the label component, that loops through
@@ -41,6 +42,11 @@ impl SpinnerVariant {
match self {
SpinnerVariant::Dots => vec!["", "", "", "", "", "", "", "", "", ""],
SpinnerVariant::DotsVariant => vec!["", "", "", "", "", "", "", ""],
SpinnerVariant::Sand => vec![
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "",
],
}
}
@@ -48,6 +54,7 @@ impl SpinnerVariant {
match self {
SpinnerVariant::Dots => Duration::from_millis(1000),
SpinnerVariant::DotsVariant => Duration::from_millis(1000),
SpinnerVariant::Sand => Duration::from_millis(2000),
}
}
@@ -55,6 +62,7 @@ impl SpinnerVariant {
match self {
SpinnerVariant::Dots => "spinner_label_dots",
SpinnerVariant::DotsVariant => "spinner_label_dots_variant",
SpinnerVariant::Sand => "spinner_label_dots_variant_2",
}
}
}
@@ -83,6 +91,10 @@ impl SpinnerLabel {
pub fn dots_variant() -> Self {
Self::with_variant(SpinnerVariant::DotsVariant)
}
pub fn sand() -> Self {
Self::with_variant(SpinnerVariant::Sand)
}
}
impl LabelCommon for SpinnerLabel {
@@ -185,6 +197,7 @@ impl Component for SpinnerLabel {
"Dots Variant",
SpinnerLabel::dots_variant().into_any_element(),
),
single_example("Sand Variant", SpinnerLabel::sand().into_any_element()),
];
Some(example_group(examples).vertical().into_any_element())

View File

@@ -10,7 +10,7 @@ To preview the docs locally you will need to install [mdBook](https://rust-lang.
mdbook serve docs
```
It's important to note the version number above. For an unknown reason, as of 2025-04-23, running 0.4.48 will cause odd URL behavior that breaks docs.
It's important to note the version number above. For an unknown reason, as of 2025-04-23, running 0.4.48 will cause odd URL behavior that breaks things.
Before committing, verify that the docs are formatted in the way Prettier expects with:

View File

@@ -17,6 +17,7 @@ To debug code written in a specific language, Zed needs to find a debug adapter
- [C](./languages/c.md#debugging) (built-in)
- [C++](./languages/cpp.md#debugging) (built-in)
- [Go](./languages/go.md#debugging) (built-in)
- [Java](./languages/java.md#debugging) (provided by extension)
- [JavaScript](./languages/javascript.md#debugging) (built-in)
- [PHP](./languages/php.md#debugging) (built-in)
- [Python](./languages/python.md#debugging) (built-in)

View File

@@ -19,150 +19,149 @@ Or manually download and install [OpenJDK 23](https://jdk.java.net/23/).
## Extension Install
You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
You can install by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
## Settings / Initialization Options
## Quick start and configuration
The extension will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
For the majority of users, Java support should work out of the box.
For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request).
- It is generally recommended to open projects with the Zed-project root at the Java project root folder (where you would commonly have your `pom.xml` or `build.gradle` file).
You can add these customizations to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}) or by using a `.zed/setting.json` inside your project.
- By default the extension will download and run the latest official version of JDTLS for you, but this requires Java version 21 to be available on your system via either the `$JAVA_HOME` environment variable or as a `java(.exe)` executable on your `$PATH`. If your project requires a lower Java version in the environment, you can specify a different JDK to use for running JDTLS via the `java_home` configuration option.
### Zed Java Settings
- You can provide a **custom launch script for JDTLS**, by adding an executable named `jdtls` (or `jdtls.bat` on Windows) to your `$PATH` environment variable. If this is present, the extension will skip downloading and launching a managed instance and use the one from the environment.
```json [settings]
- To support [Lombok](https://projectlombok.org/), the lombok-jar must be downloaded and registered as a Java-Agent when launching JDTLS. By default the extension automatically takes care of that, but in case you don't want that you can set the `lombok_support` configuration-option to `false`.
Here is a common `settings.json` including the above mentioned configurations:
```jsonc
{
"lsp": {
"jdtls": {
"initialization_options": {}
}
}
"settings": {
"java_home": "/path/to/your/JDK21+",
"lombok_support": true,
},
},
},
}
```
## Example Configs
## Debugging
### JDTLS Binary
Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Debugger Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed.
By default, zed will look in your `PATH` for a `jdtls` binary, if you wish to specify an explicit binary you can do so via settings:
To get started with Java, click the `edit debug.json` button in the Debug menu, and replace the contents of the file with the following:
```json [settings]
"lsp": {
"jdtls": {
"binary": {
"path": "/path/to/java/bin/jdtls",
// "arguments": [],
// "env": {},
"ignore_system_version": true
}
}
}
```jsonc
[
{
"adapter": "Java",
"request": "launch",
"label": "Launch Debugger",
// if your project has multiple entry points, specify the one to use:
// "mainClass": "com.myorganization.myproject.MyMainClass",
//
// this effectively sets a breakpoint at your program entry:
"stopOnEntry": true,
// the working directory for the debug process
"cwd": "$ZED_WORKTREE_ROOT",
},
]
```
### Zed Java Initialization Options
You should then be able to start a new Debug Session with the "Launch Debugger" scenario from the debug menu.
There are also many more options you can pass directly to the language server, for example:
## Launch Scripts (aka Tasks) in Windows
```json [settings]
This extension provides tasks for running your application and tests from within Zed via little play buttons next to tests/entry points. However, due to current limitations of Zed's extension interface, we can not provide scripts that will work across Maven and Gradle on both Windows and Unix-compatible systems, so out of the box the launch scripts only work on Mac and Linux.
There is a fairly straightforward fix that you can apply to make it work on Windows by supplying your own task scripts. Please see [this Issue](https://github.com/zed-extensions/java/issues/94) for information on how to do that and read the [Tasks section in Zeds documentation](https://zed.dev/docs/tasks) for more information.
## Advanced Configuration/JDTLS initialization Options
JDTLS provides many configuration options that can be passed via the `initialize` LSP-request. The extension will pass the JSON-object from `lsp.jdtls.settings.initialization_options` in your settings on to JDTLS. Please refer to the [JDTLS Configuration Wiki Page](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request) for the available options and values. Below is an example `settings.json` that would pass on the example configuration from the above wiki page to JDTLS:
```jsonc
{
"lsp": {
"jdtls": {
"initialization_options": {
"bundles": [],
"workspaceFolders": ["file:///home/snjeza/Project"],
"settings": {
"java": {
"home": "/usr/local/jdk-9.0.1",
"errors": {
"incompleteClasspath": {
"severity": "warning"
}
},
"configuration": {
"updateBuildConfiguration": "interactive",
"maven": {
"userSettings": null
}
},
"trace": {
"server": "verbose"
},
"import": {
"gradle": {
"enabled": true
"settings": {
// this will be sent to JDTLS as initializationOptions:
"initialization_options": {
"bundles": [],
// use this if your zed project root folder is not the same as the java project root:
"workspaceFolders": ["file:///home/snjeza/Project"],
"settings": {
"java": {
"home": "/usr/local/jdk-9.0.1",
"errors": {
"incompleteClasspath": {
"severity": "warning",
},
},
"maven": {
"enabled": true
"configuration": {
"updateBuildConfiguration": "interactive",
"maven": {
"userSettings": null,
},
},
"import": {
"gradle": {
"enabled": true,
},
"maven": {
"enabled": true,
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**",
"/**/test/**",
],
},
"referencesCodeLens": {
"enabled": false,
},
"signatureHelp": {
"enabled": false,
},
"implementationCodeLens": "all",
"format": {
"enabled": true,
},
"saveActions": {
"organizeImports": false,
},
"contentProvider": {
"preferred": null,
},
"autobuild": {
"enabled": false,
},
"completion": {
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*",
],
"importOrder": ["java", "javax", "com", "org"],
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**",
"/**/test/**"
]
},
"jdt": {
"ls": {
"lombokSupport": {
"enabled": false // Set this to true to enable lombok support
}
}
},
"referencesCodeLens": {
"enabled": false
},
"signatureHelp": {
"enabled": false
},
"implementationsCodeLens": {
"enabled": false
},
"format": {
"enabled": true
},
"saveActions": {
"organizeImports": false
},
"contentProvider": {
"preferred": null
},
"autobuild": {
"enabled": false
},
"completion": {
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*"
],
"importOrder": ["java", "javax", "com", "org"]
}
}
}
}
}
}
},
},
},
},
},
}
```
## Manual JDTLS Install
If you prefer, you can install JDTLS yourself and the extension can be configured to use that instead.
- macOS: `brew install jdtls`
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
Or manually download install:
- [JDTLS Milestone Builds](http://download.eclipse.org/jdtls/milestones/) (updated every two weeks)
- [JDTLS Snapshot Builds](https://download.eclipse.org/jdtls/snapshots/) (frequent updates)
## See also
- [Zed Java Repo](https://github.com/zed-extensions/java)
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
[Zed Java Repo](https://github.com/zed-extensions/java)
[Eclipse JDTLS Repo](https://github.com/eclipse-jdtls/eclipse.jdt.ls)

View File

@@ -71,6 +71,18 @@ To switch to `ruby-lsp`, add the following to your `settings.json`:
"languages": {
"Ruby": {
"language_servers": ["ruby-lsp", "!solargraph", "!rubocop", "..."]
},
// Enable herb and ruby-lsp for *.html.erb files
"HTML+ERB": {
"language_servers": ["herb", "ruby-lsp", "..."]
},
// Enable ruby-lsp for *.js.erb files
"JS+ERB": {
"language_servers": ["ruby-lsp", "..."]
},
// Enable ruby-lsp for *.yaml.erb files
"YAML+ERB": {
"language_servers": ["ruby-lsp", "..."]
}
}
}

View File

@@ -1,33 +1,33 @@
# #!/bin/bash
set -euxo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <branch-name> <commit-sha>"
if [ "$#" -ne 3 ]; then
echo "Usage: $0 <branch-name> <commit-sha> <channel>"
exit 1
fi
BRANCH_NAME="$1"
COMMIT_SHA="$2"
CHANNEL="$3"
SHORT_SHA="${COMMIT_SHA:0:8}"
NEW_BRANCH="cherry-pick-${BRANCH_NAME}-${SHORT_SHA}"
git fetch origin
git checkout "$BRANCH_NAME"
git checkout -b "$NEW_BRANCH"
git fetch --depth 2 origin +${COMMIT_SHA} ${BRANCH_NAME}
git checkout --force "origin/$BRANCH_NAME" -B "$NEW_BRANCH"
git cherry-pick "$COMMIT_SHA"
git push origin "$NEW_BRANCH"
git push origin -f "$NEW_BRANCH"
COMMIT_TITLE=$(git log -1 --pretty=format:"%s" "$COMMIT_SHA")
COMMIT_BODY=$(git log -1 --pretty=format:"%b" "$COMMIT_SHA")
# Check if commit title ends with (#number)
if [[ "$COMMIT_TITLE" =~ \(#([0-9]+)\)$ ]]; then
PR_NUMBER="${BASH_REMATCH[1]}"
PR_BODY="Cherry-pick of #${PR_NUMBER}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}"
PR_BODY="Cherry-pick of #${PR_NUMBER} to ${CHANNEL}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}"
else
PR_BODY="Cherry-pick of ${COMMIT_SHA}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}"
PR_BODY="Cherry-pick of ${COMMIT_SHA} to ${CHANNEL}"$'\n'$'\n'"----"$'\n'"${COMMIT_BODY}"
fi
# Create a pull request
gh pr create --base "$BRANCH_NAME" --head "$NEW_BRANCH" --title "$COMMIT_TITLE (cherry-pick)" --body "$PR_BODY"
gh pr create --base "$BRANCH_NAME" --head "$NEW_BRANCH" --title "$COMMIT_TITLE (cherry-pick to $CHANNEL)" --body "$PR_BODY"

View File

@@ -3,31 +3,57 @@ use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{self, NamedJob, named},
vars::Input,
vars::{self, Input, StepOutput},
};
pub fn cherry_pick() -> Workflow {
let branch = Input::string("branch", None);
let commit = Input::string("commit", None);
let cherry_pick = run_cherry_pick(&branch, &commit);
let channel = Input::string("channel", None);
let cherry_pick = run_cherry_pick(&branch, &commit, &channel);
named::workflow()
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default()
.add_input(commit.name, commit.input())
.add_input(branch.name, branch.input()),
.add_input(branch.name, branch.input())
.add_input(channel.name, channel.input()),
))
.add_job(cherry_pick.name, cherry_pick.job)
}
fn run_cherry_pick(branch: &Input, commit: &Input) -> NamedJob {
fn cherry_pick(branch: &str, commit: &str) -> Step<Run> {
named::bash(&format!("./scripts/cherry-pick {branch} {commit}"))
fn run_cherry_pick(branch: &Input, commit: &Input, channel: &Input) -> NamedJob {
fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
let step = named::uses(
"actions",
"create-github-app-token",
"bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
) // v2
.add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
.add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
.id("get-app-token");
let output = StepOutput::new(&step, "token");
(step, output)
}
fn cherry_pick(
branch: &Input,
commit: &Input,
channel: &Input,
token: &StepOutput,
) -> Step<Run> {
named::bash(&format!("./script/cherry-pick {branch} {commit} {channel}"))
.add_env(("GIT_COMMITTER_NAME", "Zed Zippy"))
.add_env(("GIT_COMMITTER_EMAIL", "hi@zed.dev"))
.add_env(("GITHUB_TOKEN", token))
}
let (authenticate, token) = authenticate_as_zippy();
named::job(
Job::default()
.runs_on(runners::LINUX_SMALL)
.add_step(steps::checkout_repo())
.add_step(cherry_pick(&branch.var(), &commit.var())),
.add_step(authenticate)
.add_step(cherry_pick(branch, commit, channel, &token)),
)
}

View File

@@ -11,28 +11,35 @@ use crate::tasks::workflows::{
pub fn compare_perf() -> Workflow {
let head = Input::string("head", None);
let base = Input::string("base", None);
let run_perf = run_perf(&base, &head);
let crate_name = Input::string("crate_name", Some("".to_owned()));
let run_perf = run_perf(&base, &head, &crate_name);
named::workflow()
.on(Event::default().workflow_dispatch(
WorkflowDispatch::default()
.add_input(head.name, head.input())
.add_input(base.name, base.input()),
.add_input(base.name, base.input())
.add_input(crate_name.name, crate_name.input()),
))
.add_job(run_perf.name, run_perf.job)
}
pub fn run_perf(base: &Input, head: &Input) -> NamedJob {
fn cargo_perf_test(ref_name: String) -> Step<Run> {
// TODO: vim not gpui, and ideally allow args
named::bash(&format!("cargo perf-test -p gpui -- --json={ref_name}"))
pub fn run_perf(base: &Input, head: &Input, crate_name: &Input) -> NamedJob {
fn cargo_perf_test(ref_name: &Input, crate_name: &Input) -> Step<Run> {
named::bash(&format!(
"
if [ -n \"{crate_name}\" ]; then
cargo perf-test -p {crate_name} -- --json={ref_name};
else
cargo perf-test -p vim -- --json={ref_name};
fi"
))
}
fn install_hyperfine() -> Step<Run> {
named::bash("cargo install hyperfine")
}
fn compare_runs(head: String, base: String) -> Step<Run> {
// TODO: this should really be swapped...
fn compare_runs(head: &Input, base: &Input) -> Step<Run> {
named::bash(&format!(
"cargo perf-compare --save=results.md {base} {head}"
))
@@ -45,11 +52,11 @@ pub fn run_perf(base: &Input, head: &Input) -> NamedJob {
.add_step(steps::setup_cargo_config(runners::Platform::Linux))
.map(steps::install_linux_dependencies)
.add_step(install_hyperfine())
.add_step(steps::git_checkout(&base.var()))
.add_step(cargo_perf_test(base.var()))
.add_step(steps::git_checkout(&head.var()))
.add_step(cargo_perf_test(head.var()))
.add_step(compare_runs(head.var(), base.var()))
.add_step(steps::git_checkout(base))
.add_step(cargo_perf_test(base, crate_name))
.add_step(steps::git_checkout(head))
.add_step(cargo_perf_test(head, crate_name))
.add_step(compare_runs(head, base))
.add_step(upload_artifact("results.md"))
.add_step(steps::cleanup_cargo_config(runners::Platform::Linux)),
)

View File

@@ -99,11 +99,7 @@ fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
dependant_job(deps)
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(indoc::indoc!(
r#"
false
&& startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
"# // todo(ci-release) enable
r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"#
)))
.add_step(
steps::script(

View File

@@ -299,7 +299,7 @@ pub(crate) mod named {
}
}
pub fn git_checkout(ref_name: &str) -> Step<Run> {
pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
named::bash(&format!(
"git fetch origin {ref_name} && git checkout {ref_name}"
))

View File

@@ -1,6 +1,6 @@
use std::cell::RefCell;
use gh_workflow::{Concurrency, Env, Expression, WorkflowDispatchInput};
use gh_workflow::{Concurrency, Env, Expression, Step, WorkflowDispatchInput};
use crate::tasks::workflows::{runners::Platform, steps::NamedJob};
@@ -34,6 +34,8 @@ secret!(ZED_CLIENT_CHECKSUM_SEED);
secret!(ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON);
secret!(ZED_SENTRY_MINIDUMP_ENDPOINT);
secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN);
secret!(ZED_ZIPPY_APP_ID);
secret!(ZED_ZIPPY_APP_PRIVATE_KEY);
// todo(ci) make these secrets too...
var!(AZURE_SIGNING_ACCOUNT_NAME);
@@ -116,6 +118,30 @@ impl PathCondition {
}
}
pub(crate) struct StepOutput {
name: &'static str,
step_id: String,
}
impl StepOutput {
pub fn new<T>(step: &Step<T>, name: &'static str) -> Self {
Self {
name,
step_id: step
.value
.id
.clone()
.expect("Steps that produce outputs must have an ID"),
}
}
}
impl std::fmt::Display for StepOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "${{{{ steps.{}.outputs.{} }}}}", self.step_id, self.name)
}
}
pub(crate) struct Input {
pub input_type: &'static str,
pub name: &'static str,
@@ -131,10 +157,6 @@ impl Input {
}
}
pub fn var(&self) -> String {
format!("${{{{ inputs.{} }}}}", self.name)
}
pub fn input(&self) -> WorkflowDispatchInput {
WorkflowDispatchInput {
description: self.name.to_owned(),
@@ -145,6 +167,12 @@ impl Input {
}
}
impl std::fmt::Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "${{{{ inputs.{} }}}}", self.name)
}
}
pub mod assets {
// NOTE: these asset names also exist in the zed.dev codebase.
pub const MAC_AARCH64: &str = "Zed-aarch64.dmg";