Compare commits
21 Commits
test-threa
...
diff-perf-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfeb67f93c | ||
|
|
0f5a63a9b0 | ||
|
|
c8ada5b1ae | ||
|
|
27a18843d4 | ||
|
|
2bc1d60c52 | ||
|
|
17933f1222 | ||
|
|
cd87307289 | ||
|
|
11b29d693f | ||
|
|
c061698229 | ||
|
|
b4f7af066e | ||
|
|
c83621fa1f | ||
|
|
0da52d6774 | ||
|
|
60ee0dd19b | ||
|
|
9fc4abd8de | ||
|
|
2ead8c42fb | ||
|
|
0a4b1ac696 | ||
|
|
f9fb855990 | ||
|
|
b587a62ac3 | ||
|
|
1b2e38bb33 | ||
|
|
4339c772e4 | ||
|
|
ba7ea71c00 |
16
.github/workflows/cherry_pick.yml
vendored
16
.github/workflows/cherry_pick.yml
vendored
@@ -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 }}
|
||||
|
||||
20
.github/workflows/compare_perf.yml
vendored
20
.github/workflows/compare_perf.yml
vendored
@@ -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 }}
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1047,6 +1047,7 @@ impl X11Client {
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
prefer_character_input: false,
|
||||
}));
|
||||
}
|
||||
Event::KeyRelease(event) => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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", "..."]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}"
|
||||
))
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user