Compare commits

...

21 Commits

Author SHA1 Message Date
Joseph T. Lyons
c70617c69e zed 0.212.2 2025-11-07 11:25:35 -05:00
zed-zippy[bot]
17afd49adb settings_ui: Use any open workspace window when opening settings links (#42106) (cherry-pick to preview) (#42196)
Cherry-pick of #42106 to preview

----
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-07 09:49:54 -05:00
zed-zippy[bot]
6984c9b459 workspace: Do not panic when the database is corruped (#42186) (cherry-pick to preview) (#42189)
Cherry-pick of #42186 to preview

----
Fixes ZED-1NK

Release Notes:

- Fixed zed not starting when the database cannot be loaded

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 13:22:52 +00:00
Lukas Wirth
be8038eb81 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-07 13:31:12 +01:00
Lukas Wirth
577467bed8 agent_ui: Do not show Codex wsl warning on wsl take 2 (#42096)
https://github.com/zed-industries/zed/pull/42079#discussion_r2498472887

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 13:30:38 +01:00
Lukas Wirth
18ddbc044f agent_ui: Do not show Codex wsl warning on wsl (#42079)
Release Notes:

- Fixed the codex wsl warning being shown on wsl itself
2025-11-07 13:30:30 +01:00
Lukas Wirth
5e1da57f25 remote: Fix detect_can_exec detection (#42087)
Closes https://github.com/zed-industries/zed/issues/42036

Release Notes:

- Fixed an issuer with wsl exec detection eagerly failing, breaking
remote connections
2025-11-07 13:30:21 +01:00
Lukas Wirth
4bee58f70e util: Fix shell environment fetching with cmd (#42093)
Release Notes:

- Fixed shell environment fetching failing when having `cmd` configured
as terminal shell
2025-11-07 13:30:11 +01:00
Lukas Wirth
7eda0cd996 zlog: Add env var to enable line number logging (#41905)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 13:29:37 +01:00
zed-zippy[bot]
c21c47e8ef remote: Flush to stdin when writing to sftp 2 (#42126) (cherry-pick to preview) (#42187)
Cherry-pick of #42126 to preview

----
https://github.com/zed-industries/zed/pull/42103#issuecomment-3498137130

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 13:28:41 +01:00
zed-zippy[bot]
92142fc2b2 diagnostics: Fix diagnostics view no clearing blocks correctly (#42179) (cherry-pick to preview) (#42181)
Cherry-pick of #42179 to preview

----
Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 11:25:39 +00:00
zed-zippy[bot]
ed0121dc3a project: Remove unnecessary panic (#42167) (cherry-pick to preview) (#42168)
Cherry-pick of #42167 to preview

----
If we are in a remote session with the remote dropped, this path is very
much reachable if the call to this function got queued up in a task.

Fixes ZED-124

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 07:07:06 +00:00
Zed Bot
3aeeed0b30 Bump to 0.212.1 for @smitbarmase 2025-11-06 15:03:25 +00:00
Smit Barmase
7e6cdabb26 language: Fix completion menu no longer prioritizes relevant items for Typescript and Python (#42065)
Closes #41672

Regressed in https://github.com/zed-industries/zed/pull/40242

Release Notes:

- Fixed issue where completion menu no longer prioritizes relevant items
for TypeScript and Python.
2025-11-06 13:37:37 +05:30
Richard Feldman
f91f3f24ef Run ACP login from same cwd as agent server (#42038)
This makes it possible to do login via things like `cmd: "node", args:
["my-node-file.js", "login"]`

Also, that command will now use Zed's managed `node` instance.

Release Notes:

- ACP extensions can now run terminal login commands using relative
paths
2025-11-06 08:08:30 +01:00
Danilo Leal
4ebc20b30c agent_ui: Fix how icons from external agents are displayed (#42034)
Release Notes:

- N/A
2025-11-06 08:08:09 +01:00
Danilo Leal
ac6aa735e4 gpui: Add support for rendering SVG from external files (#42024)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-06 08:07:31 +01:00
zed-zippy[bot]
d2cb9a13b8 Refresh zed.dev releases page after releases (#42060) (cherry-pick to preview) (#42063)
Cherry-pick of #42060 to preview

----
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-06 06:47:58 +00:00
zed-zippy[bot]
5956489a47 Fix generate release notes script on first stable (#42061) (cherry-pick to preview) (#42062)
Cherry-pick of #42061 to preview

----
Don't crash in generate-release-notes on the first stable
commit on a branch.

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-06 06:37:43 +00:00
John Tur
418d850375 Fix corrupted characters being inserted when Alt is pressed (#42033)
The Alt+Numpad buffer that's maintained by the input stack is getting
corrupted, leading to garbage characters being inserted on keystrokes
like Alt+Up. Disable the automatic handling of Alt+Numpad for now until
the cause of this corruption is understood. The Alt+Numpad input did not
work anyway, so this does not regress anything.

Release Notes:

- windows: Fixed corrupted characters being inserted when Alt is pressed
(preview only)
2025-11-05 16:11:55 -05:00
Joseph T. Lyons
7ad3cf4387 v0.212.x preview 2025-11-05 12:07:10 -05:00
37 changed files with 748 additions and 456 deletions

69
.github/workflows/after_release.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
# Generated from xtask::workflows::after_release
# Rebuild with `cargo xtask workflows`.
name: after_release
on:
release:
types:
- published
jobs:
rebuild_releases_page:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: after_release::rebuild_releases_page
run: 'curl https://zed.dev/api/revalidate-releases -H "Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}"'
shell: bash -euxo pipefail {0}
env:
RELEASE_NOTES_API_TOKEN: ${{ secrets.RELEASE_NOTES_API_TOKEN }}
post_to_discord:
needs:
- rebuild_releases_page
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-release-url
name: after_release::post_to_discord::get_release_url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- id: get-content
name: after_release::post_to_discord::get_content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: '...'
- name: after_release::post_to_discord::discord_webhook_action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish_winget:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: set-package-name
name: after_release::publish_winget::set_package_name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,93 +0,0 @@
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
# TO BOTH STABLE AND PREVIEW CHANNELS
name: Release Actions
on:
release:
types: [published]
jobs:
discord_release:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
else
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d @release_data.json

3
Cargo.lock generated
View File

@@ -20951,6 +20951,7 @@ dependencies = [
"gh-workflow",
"indexmap 2.11.4",
"indoc",
"serde",
"toml 0.8.23",
"toml_edit 0.22.27",
]
@@ -21135,7 +21136,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.212.0"
version = "0.212.2"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -294,7 +294,6 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool,
}
@@ -401,7 +400,6 @@ impl AcpThreadView {
),
];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex);
@@ -447,7 +445,6 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning,
}
}
@@ -1506,6 +1503,12 @@ impl AcpThreadView {
})
.unwrap_or_default();
// Run SpawnInTerminal in the same dir as the ACP server
let cwd = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
// Build SpawnInTerminal from _meta
let login = task::SpawnInTerminal {
id: task::TaskId(format!("external-agent-{}-login", label)),
@@ -1514,6 +1517,7 @@ impl AcpThreadView {
command: Some(command.to_string()),
args,
command_label: label.to_string(),
cwd,
env,
use_new_terminal: true,
allow_concurrent_runs: true,
@@ -1526,8 +1530,9 @@ impl AcpThreadView {
pending_auth_method.replace(method.clone());
if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone();
let authenticate = Self::spawn_external_agent_login(
login, workspace, false, window, cx,
login, workspace, project, false, true, window, cx,
);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
@@ -1671,7 +1676,10 @@ impl AcpThreadView {
&& let Some(login) = self.login.clone()
{
if let Some(workspace) = self.workspace.upgrade() {
Self::spawn_external_agent_login(login, workspace, false, window, cx)
let project = self.project.clone();
Self::spawn_external_agent_login(
login, workspace, project, false, false, window, cx,
)
} else {
Task::ready(Ok(()))
}
@@ -1721,17 +1729,40 @@ impl AcpThreadView {
fn spawn_external_agent_login(
login: task::SpawnInTerminal,
workspace: Entity<Workspace>,
project: Entity<Project>,
previous_attempt: bool,
check_exit_code: bool,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| {
let mut task = login.clone();
if let Some(cmd) = &task.command {
// Have "node" command use Zed's managed Node runtime by default
if cmd == "node" {
let resolved_node_runtime = project
.update(cx, |project, cx| {
let agent_server_store = project.agent_server_store().clone();
agent_server_store.update(cx, |store, cx| {
store.node_runtime().map(|node_runtime| {
cx.background_spawn(async move {
node_runtime.binary_path().await
})
})
})
});
if let Ok(Some(resolve_task)) = resolved_node_runtime {
if let Ok(node_path) = resolve_task.await {
task.command = Some(node_path.to_string_lossy().to_string());
}
}
}
}
task.shell = task::Shell::WithArguments {
program: task.command.take().expect("login command should be set"),
args: std::mem::take(&mut task.args),
@@ -1749,44 +1780,65 @@ impl AcpThreadView {
})?;
let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
if check_exit_code {
// For extension-based auth, wait for the process to exit and check exit code
let exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
match exit_status {
Some(status) if status.success() => {
Ok(())
}
Some(status) => {
Err(anyhow!("Login command failed with exit code: {:?}", status.code()))
}
None => {
Err(anyhow!("Login command terminated without exit status"))
}
}
} else {
// For hardcoded agents (claude-login, gemini-cli): look for specific output
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
}
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
}
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
})
}
@@ -5197,7 +5249,6 @@ impl AcpThreadView {
)
}
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning {
Some(
@@ -5213,8 +5264,9 @@ impl AcpThreadView {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
move |_, _, _window, cx| {
#[cfg(windows)]
_window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx,
);
@@ -5717,13 +5769,10 @@ impl Render for AcpThreadView {
})
.children(self.render_thread_retry_status_callout(window, cx))
.children({
#[cfg(target_os = "windows")]
{
if cfg!(windows) && self.project.read(cx).is_local() {
self.render_codex_windows_warning(cx)
}
#[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
} else {
None
}
})
.children(self.render_thread_error(cx))

View File

@@ -2081,7 +2081,7 @@ impl AgentPanel {
let mut entry =
ContextMenuEntry::new(format!("New {}", agent_name));
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_path(icon_path);
entry = entry.custom_icon_svg(icon_path);
} else {
entry = entry.icon(IconName::Terminal);
}
@@ -2150,7 +2150,7 @@ impl AgentPanel {
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::from_path(icon_path).color(Color::Muted))
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})

View File

@@ -152,8 +152,8 @@ impl Render for ProjectDiagnosticsEditor {
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum RetainExcerpts {
Yes,
No,
All,
Dirty,
}
impl ProjectDiagnosticsEditor {
@@ -207,17 +207,7 @@ impl ProjectDiagnosticsEditor {
"diagnostics updated for server {language_server_id}, \
paths {paths:?}. updating excerpts"
);
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx);
this.update_stale_excerpts(
if focused {
RetainExcerpts::Yes
} else {
RetainExcerpts::No
},
window,
cx,
);
this.update_stale_excerpts(window, cx);
}
_ => {}
},
@@ -280,8 +270,7 @@ impl ProjectDiagnosticsEditor {
cx,
)
});
this.diagnostics.clear();
this.update_all_excerpts(window, cx);
this.refresh(window, cx);
})
.detach();
@@ -301,14 +290,14 @@ impl ProjectDiagnosticsEditor {
diagnostic_summary_update: Task::ready(()),
_subscription: project_event_subscription,
};
this.update_all_excerpts(window, cx);
this.refresh(window, cx);
this
}
/// Closes all excerpts of buffers that:
/// - have no diagnostics anymore
/// - are saved (not dirty)
/// - and, if `reatin_selections` is true, do not have selections within them
/// - and, if `retain_selections` is true, do not have selections within them
fn close_diagnosticless_buffers(
&mut self,
_window: &mut Window,
@@ -328,19 +317,19 @@ impl ProjectDiagnosticsEditor {
if retain_selections && selected_buffers.contains(&buffer_id) {
continue;
}
let has_blocks = self
let has_no_blocks = self
.blocks
.get(&buffer_id)
.is_none_or(|blocks| blocks.is_empty());
if !has_blocks {
if !has_no_blocks {
continue;
}
let is_dirty = self
.multibuffer
.read(cx)
.buffer(buffer_id)
.is_some_and(|buffer| buffer.read(cx).is_dirty());
if !is_dirty {
.is_none_or(|buffer| buffer.read(cx).is_dirty());
if is_dirty {
continue;
}
self.multibuffer.update(cx, |b, cx| {
@@ -349,18 +338,10 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_stale_excerpts(
&mut self,
mut retain_excerpts: RetainExcerpts,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.update_excerpts_task.is_some() {
return;
}
if self.multibuffer.read(cx).is_dirty(cx) {
retain_excerpts = RetainExcerpts::Yes;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
@@ -386,6 +367,13 @@ impl ProjectDiagnosticsEditor {
.log_err()
{
this.update_in(cx, |this, window, cx| {
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx);
let retain_excerpts = if focused {
RetainExcerpts::All
} else {
RetainExcerpts::Dirty
};
this.update_excerpts(buffer, retain_excerpts, window, cx)
})?
.await?;
@@ -441,7 +429,7 @@ impl ProjectDiagnosticsEditor {
if self.update_excerpts_task.is_some() {
self.update_excerpts_task = None;
} else {
self.update_all_excerpts(window, cx);
self.refresh(window, cx);
}
cx.notify();
}
@@ -459,31 +447,26 @@ impl ProjectDiagnosticsEditor {
}
}
/// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
/// Clears all diagnostics in this view, and refetches them from the project.
fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.diagnostics.clear();
self.editor.update(cx, |editor, cx| {
for (_, block_ids) in self.blocks.drain() {
editor.display_map.update(cx, |display_map, cx| {
display_map.remove_blocks(block_ids.into_iter().collect(), cx)
});
}
});
self.multibuffer
.update(cx, |multibuffer, cx| multibuffer.clear(cx));
self.project.update(cx, |project, cx| {
let mut project_paths = project
self.paths_to_update = project
.diagnostic_summaries(false, cx)
.map(|(project_path, _, _)| project_path)
.collect::<BTreeSet<_>>();
self.multibuffer.update(cx, |multibuffer, cx| {
for buffer in multibuffer.all_buffers() {
if let Some(file) = buffer.read(cx).file() {
project_paths.insert(ProjectPath {
path: file.path().clone(),
worktree_id: file.worktree_id(cx),
});
}
}
multibuffer.clear(cx);
});
self.paths_to_update = project_paths;
});
self.update_stale_excerpts(RetainExcerpts::No, window, cx);
self.update_stale_excerpts(window, cx);
}
fn diagnostics_are_unchanged(
@@ -576,21 +559,24 @@ impl ProjectDiagnosticsEditor {
blocks.extend(more);
}
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = match retain_excerpts {
RetainExcerpts::No => Vec::new(),
RetainExcerpts::Yes => this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
multi_buffer
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
let is_dirty = multi_buffer
.buffer(buffer_id)
.is_none_or(|buffer| buffer.read(cx).is_dirty());
match retain_excerpts {
RetainExcerpts::Dirty if !is_dirty => Vec::new(),
RetainExcerpts::All | RetainExcerpts::Dirty => multi_buffer
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
.map(|(_, range)| ExcerptRange {
context: range.context.to_point(&buffer_snapshot),
primary: range.primary.to_point(&buffer_snapshot),
})
.collect()
})
})?,
};
.collect(),
}
})
})?;
let mut result_blocks = vec![None; excerpt_ranges.len()];
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks {
@@ -946,7 +932,7 @@ impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_all_excerpts(window, cx);
project_diagnostics_editor.refresh(window, cx);
});
}

View File

@@ -769,7 +769,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
log::info!("constructing reference diagnostics view");
@@ -968,7 +968,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()

View File

@@ -173,6 +173,10 @@ pub struct AgentServerManifestEntry {
/// cmd = "node"
/// args = ["index.js", "--port", "3000"]
/// ```
///
/// Note: All commands are executed with the archive extraction directory as the
/// working directory, so relative paths in args (like "index.js") will resolve
/// relative to the extracted archive contents.
pub targets: HashMap<String, TargetConfig>,
}

View File

@@ -1,5 +1,7 @@
use std::{fs, path::Path, sync::Arc};
use crate::{
App, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
App, Asset, Bounds, Element, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString, Size,
StyleRefinement, Styled, TransformationMatrix, Window, geometry::Negate as _, point, px,
radians, size,
@@ -11,6 +13,7 @@ pub struct Svg {
interactivity: Interactivity,
transformation: Option<Transformation>,
path: Option<SharedString>,
external_path: Option<SharedString>,
}
/// Create a new SVG element.
@@ -20,6 +23,7 @@ pub fn svg() -> Svg {
interactivity: Interactivity::new(),
transformation: None,
path: None,
external_path: None,
}
}
@@ -30,6 +34,12 @@ impl Svg {
self
}
/// Set the path to the SVG file for this element.
pub fn external_path(mut self, path: impl Into<SharedString>) -> Self {
self.external_path = Some(path.into());
self
}
/// Transform the SVG element with the given transformation.
/// Note that this won't effect the hitbox or layout of the element, only the rendering.
pub fn with_transformation(mut self, transformation: Transformation) -> Self {
@@ -117,7 +127,35 @@ impl Element for Svg {
.unwrap_or_default();
window
.paint_svg(bounds, path.clone(), transformation, color, cx)
.paint_svg(bounds, path.clone(), None, transformation, color, cx)
.log_err();
} else if let Some((path, color)) =
self.external_path.as_ref().zip(style.text.color)
{
let Some(bytes) = window
.use_asset::<SvgAsset>(path, cx)
.and_then(|asset| asset.log_err())
else {
return;
};
let transformation = self
.transformation
.as_ref()
.map(|transformation| {
transformation.into_matrix(bounds.center(), window.scale_factor())
})
.unwrap_or_default();
window
.paint_svg(
bounds,
path.clone(),
Some(&bytes),
transformation,
color,
cx,
)
.log_err();
}
},
@@ -219,3 +257,21 @@ impl Transformation {
.translate(center.scale(scale_factor).negate())
}
}
enum SvgAsset {}
impl Asset for SvgAsset {
type Source = SharedString;
type Output = Result<Arc<[u8]>, Arc<std::io::Error>>;
fn load(
source: Self::Source,
_cx: &mut App,
) -> impl Future<Output = Self::Output> + Send + 'static {
async move {
let bytes = fs::read(Path::new(source.as_ref())).map_err(|e| Arc::new(e))?;
let bytes = Arc::from(bytes);
Ok(bytes)
}
}
}

View File

@@ -281,7 +281,7 @@ impl BackgroundExecutor {
});
let mut cx = std::task::Context::from_waker(&waker);
let duration = Duration::from_secs(500);
let duration = Duration::from_secs(180);
let mut test_should_end_by = Instant::now() + duration;
loop {

View File

@@ -1370,7 +1370,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
scan_code as u32,
Some(&keyboard_state),
&mut buffer_c,
0x4,
0x5,
)
};
if result_c < 0 {
@@ -1415,7 +1415,7 @@ fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
scan_code as u32,
Some(&state_no_modifiers),
&mut buffer_c_no_modifiers,
0x4,
0x5,
)
};
if result_c_no_modifiers <= 0 {

View File

@@ -95,27 +95,34 @@ impl SvgRenderer {
pub(crate) fn render_alpha_mask(
&self,
params: &RenderSvgParams,
bytes: Option<&[u8]>,
) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
// Load the tree.
let Some(bytes) = self.asset_source.load(&params.path)? else {
return Ok(None);
let render_pixmap = |bytes| {
let pixmap = self.render_pixmap(bytes, SvgSize::Size(params.size))?;
// Convert the pixmap's pixels into an alpha mask.
let size = Size::new(
DevicePixels(pixmap.width() as i32),
DevicePixels(pixmap.height() as i32),
);
let alpha_mask = pixmap
.pixels()
.iter()
.map(|p| p.alpha())
.collect::<Vec<_>>();
Ok(Some((size, alpha_mask)))
};
let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
// Convert the pixmap's pixels into an alpha mask.
let size = Size::new(
DevicePixels(pixmap.width() as i32),
DevicePixels(pixmap.height() as i32),
);
let alpha_mask = pixmap
.pixels()
.iter()
.map(|p| p.alpha())
.collect::<Vec<_>>();
Ok(Some((size, alpha_mask)))
if let Some(bytes) = bytes {
render_pixmap(bytes)
} else if let Some(bytes) = self.asset_source.load(&params.path)? {
render_pixmap(&bytes)
} else {
Ok(None)
}
}
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {

View File

@@ -3084,6 +3084,7 @@ impl Window {
&mut self,
bounds: Bounds<Pixels>,
path: SharedString,
mut data: Option<&[u8]>,
transformation: TransformationMatrix,
color: Hsla,
cx: &App,
@@ -3104,7 +3105,8 @@ impl Window {
let Some(tile) =
self.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(&params)? else {
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(&params, data)?
else {
return Ok(None);
};
Ok(Some((size, Cow::Owned(bytes))))

View File

@@ -2324,17 +2324,19 @@ impl CodeLabel {
}
pub fn plain(text: String, filter_text: Option<&str>) -> Self {
Self::filtered(text, filter_text, Vec::new())
Self::filtered(text.clone(), text.len(), filter_text, Vec::new())
}
pub fn filtered(
text: String,
label_len: usize,
filter_text: Option<&str>,
runs: Vec<(Range<usize>, HighlightId)>,
) -> Self {
assert!(label_len <= text.len());
let filter_range = filter_text
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..text.len());
.unwrap_or(0..label_len);
Self::new(text, filter_range, runs)
}

View File

@@ -406,6 +406,7 @@ impl LspAdapter for PyrightLspAdapter {
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let label = &item.label;
let label_len = label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
@@ -427,9 +428,10 @@ impl LspAdapter for PyrightLspAdapter {
}
Some(language::CodeLabel::filtered(
text,
label_len,
item.filter_text.as_deref(),
highlight_id
.map(|id| (0..label.len(), id))
.map(|id| (0..label_len, id))
.into_iter()
.collect(),
))
@@ -1467,6 +1469,7 @@ impl LspAdapter for PyLspAdapter {
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let label = &item.label;
let label_len = label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
@@ -1477,6 +1480,7 @@ impl LspAdapter for PyLspAdapter {
};
Some(language::CodeLabel::filtered(
label.clone(),
label_len,
item.filter_text.as_deref(),
vec![(0..label.len(), highlight_id)],
))
@@ -1742,6 +1746,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let label = &item.label;
let label_len = label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
@@ -1763,6 +1768,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
}
Some(language::CodeLabel::filtered(
text,
label_len,
item.filter_text.as_deref(),
highlight_id
.map(|id| (0..label.len(), id))

View File

@@ -754,7 +754,7 @@ impl LspAdapter for TypeScriptLspAdapter {
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
use lsp::CompletionItemKind as Kind;
let len = item.label.len();
let label_len = item.label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
@@ -779,8 +779,9 @@ impl LspAdapter for TypeScriptLspAdapter {
};
Some(language::CodeLabel::filtered(
text,
label_len,
item.filter_text.as_deref(),
vec![(0..len, highlight_id)],
vec![(0..label_len, highlight_id)],
))
}

View File

@@ -178,7 +178,7 @@ impl LspAdapter for VtslsLspAdapter {
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
use lsp::CompletionItemKind as Kind;
let len = item.label.len();
let label_len = item.label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
@@ -203,8 +203,9 @@ impl LspAdapter for VtslsLspAdapter {
};
Some(language::CodeLabel::filtered(
text,
label_len,
item.filter_text.as_deref(),
vec![(0..len, highlight_id)],
vec![(0..label_len, highlight_id)],
))
}

View File

@@ -438,6 +438,13 @@ impl AgentServerStore {
cx.emit(AgentServersUpdated);
}
pub fn node_runtime(&self) -> Option<NodeRuntime> {
match &self.state {
AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
_ => None,
}
}
pub fn local(
node_runtime: NodeRuntime,
fs: Arc<dyn Fs>,
@@ -1560,7 +1567,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
env: Some(env),
};
Ok((command, root_dir.to_string_lossy().into_owned(), None))
Ok((command, version_dir.to_string_lossy().into_owned(), None))
})
}
@@ -1946,6 +1953,51 @@ mod extension_agent_tests {
assert_eq!(target.args, vec!["index.js"]);
}
#[gpui::test]
async fn test_commands_run_in_extraction_directory(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("test-ext"),
agent_id: Arc::from("test-agent"),
targets: {
let mut map = HashMap::default();
map.insert(
"darwin-aarch64".to_string(),
extension::TargetConfig {
archive: "https://example.com/test.zip".into(),
cmd: "node".into(),
args: vec![
"server.js".into(),
"--config".into(),
"./config.json".into(),
],
sha256: None,
},
);
map
},
env: HashMap::default(),
};
// Verify the agent is configured with relative paths in args
let target = agent.targets.get("darwin-aarch64").unwrap();
assert_eq!(target.args[0], "server.js");
assert_eq!(target.args[2], "./config.json");
// These relative paths will resolve relative to the extraction directory
// when the command is executed
}
#[test]
fn test_tilde_expansion_in_settings() {
let settings = settings::BuiltinAgentServerSettings {

View File

@@ -333,60 +333,51 @@ async fn load_directory_shell_environment(
.into()
};
if cfg!(target_os = "windows") {
let (shell, args) = shell.program_and_args();
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
.await
.with_context(|| {
tx.unbounded_send("Failed to load environment variables".into())
.ok();
format!("capturing shell environment with {shell:?}")
})?;
if cfg!(target_os = "windows")
&& let Some(path) = envs.remove("Path")
{
// windows env vars are case-insensitive, so normalize the path var
// so we can just assume `PATH` in other places
envs.insert("PATH".into(), path);
}
// If the user selects `Direct` for direnv, it would set an environment
// variable that later uses to know that it should not run the hook.
// We would include in `.envs` call so it is okay to run the hook
// even if direnv direct mode is enabled.
let direnv_environment = match load_direnv {
DirenvSettings::ShellHook => None,
// Note: direnv is not available on Windows, so we skip direnv processing
// and just return the shell environment
let (shell, args) = shell.program_and_args();
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
DirenvSettings::Direct if cfg!(target_os = "windows") => None,
DirenvSettings::Direct => load_direnv_environment(&envs, &dir)
.await
.with_context(|| {
tx.unbounded_send("Failed to load environment variables".into())
tx.unbounded_send("Failed to load direnv environment".into())
.ok();
format!("capturing shell environment with {shell:?}")
})?;
if let Some(path) = envs.remove("Path") {
// windows env vars are case-insensitive, so normalize the path var
// so we can just assume `PATH` in other places
envs.insert("PATH".into(), path);
}
Ok(envs)
} else {
let (shell, args) = shell.program_and_args();
let mut envs = util::shell_env::capture(shell.clone(), args, abs_path)
.await
.with_context(|| {
tx.unbounded_send("Failed to load environment variables".into())
.ok();
format!("capturing shell environment with {shell:?}")
})?;
// If the user selects `Direct` for direnv, it would set an environment
// variable that later uses to know that it should not run the hook.
// We would include in `.envs` call so it is okay to run the hook
// even if direnv direct mode is enabled.
let direnv_environment = match load_direnv {
DirenvSettings::ShellHook => None,
DirenvSettings::Direct => load_direnv_environment(&envs, &dir)
.await
.with_context(|| {
tx.unbounded_send("Failed to load direnv environment".into())
.ok();
"load direnv environment"
})
.log_err(),
};
if let Some(direnv_environment) = direnv_environment {
for (key, value) in direnv_environment {
if let Some(value) = value {
envs.insert(key, value);
} else {
envs.remove(&key);
}
"load direnv environment"
})
.log_err(),
};
if let Some(direnv_environment) = direnv_environment {
for (key, value) in direnv_environment {
if let Some(value) = value {
envs.insert(key, value);
} else {
envs.remove(&key);
}
}
Ok(envs)
}
Ok(envs)
}
async fn load_direnv_environment(

View File

@@ -4193,7 +4193,7 @@ impl LspStore {
})
.detach();
} else {
panic!("oops!");
// Our remote connection got closed
}
handle
}

View File

@@ -306,7 +306,7 @@ impl RemoteConnection for SshRemoteConnection {
use futures::AsyncWriteExt;
let sftp_batch = format!("put -r {src_path_display} {dest_path_str}\n");
stdin.write_all(sftp_batch.as_bytes()).await?;
drop(stdin);
stdin.flush().await?;
}
let output = child.output().await?;

View File

@@ -98,21 +98,10 @@ impl WslRemoteConnection {
let args = &["-m"];
let output = wsl_command_impl(options, &program, args, true)
.output()
.await?;
if !output.status.success() {
let output = wsl_command_impl(options, &program, args, false)
.output()
.await?;
if !output.status.success() {
return Err(anyhow!(
"Command '{}' failed: {}",
program,
String::from_utf8_lossy(&output.stderr).trim()
));
}
.await;
if !output.is_ok_and(|output| output.status.success()) {
run_wsl_command_impl(options, &program, args, false).await?;
Ok(false)
} else {
Ok(true)

View File

@@ -48,6 +48,7 @@ pub struct ContextMenuEntry {
label: SharedString,
icon: Option<IconName>,
custom_icon_path: Option<SharedString>,
custom_icon_svg: Option<SharedString>,
icon_position: IconPosition,
icon_size: IconSize,
icon_color: Option<Color>,
@@ -68,6 +69,7 @@ impl ContextMenuEntry {
label: label.into(),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: IconPosition::Start,
icon_size: IconSize::Small,
icon_color: None,
@@ -94,7 +96,15 @@ impl ContextMenuEntry {
pub fn custom_icon_path(mut self, path: impl Into<SharedString>) -> Self {
self.custom_icon_path = Some(path.into());
self.icon = None; // Clear IconName if custom path is set
self.custom_icon_svg = None; // Clear other icon sources if custom path is set
self.icon = None;
self
}
pub fn custom_icon_svg(mut self, svg: impl Into<SharedString>) -> Self {
self.custom_icon_svg = Some(svg.into());
self.custom_icon_path = None; // Clear other icon sources if custom path is set
self.icon = None;
self
}
@@ -396,6 +406,7 @@ impl ContextMenu {
handler: Rc::new(move |_, window, cx| handler(window, cx)),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: IconPosition::End,
icon_size: IconSize::Small,
icon_color: None,
@@ -425,6 +436,7 @@ impl ContextMenu {
handler: Rc::new(move |_, window, cx| handler(window, cx)),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: IconPosition::End,
icon_size: IconSize::Small,
icon_color: None,
@@ -454,6 +466,7 @@ impl ContextMenu {
handler: Rc::new(move |_, window, cx| handler(window, cx)),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: IconPosition::End,
icon_size: IconSize::Small,
icon_color: None,
@@ -482,6 +495,7 @@ impl ContextMenu {
handler: Rc::new(move |_, window, cx| handler(window, cx)),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: position,
icon_size: IconSize::Small,
icon_color: None,
@@ -541,6 +555,7 @@ impl ContextMenu {
}),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_position: IconPosition::End,
icon_size: IconSize::Small,
icon_color: None,
@@ -572,6 +587,7 @@ impl ContextMenu {
}),
icon: None,
custom_icon_path: None,
custom_icon_svg: None,
icon_size: IconSize::Small,
icon_position: IconPosition::End,
icon_color: None,
@@ -593,6 +609,7 @@ impl ContextMenu {
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
icon: Some(IconName::ArrowUpRight),
custom_icon_path: None,
custom_icon_svg: None,
icon_size: IconSize::XSmall,
icon_position: IconPosition::End,
icon_color: None,
@@ -913,6 +930,7 @@ impl ContextMenu {
handler,
icon,
custom_icon_path,
custom_icon_svg,
icon_position,
icon_size,
icon_color,
@@ -965,6 +983,28 @@ impl ContextMenu {
)
})
.into_any_element()
} else if let Some(custom_icon_svg) = custom_icon_svg {
h_flex()
.gap_1p5()
.when(
*icon_position == IconPosition::Start && toggle.is_none(),
|flex| {
flex.child(
Icon::from_external_svg(custom_icon_svg.clone())
.size(*icon_size)
.color(icon_color),
)
},
)
.child(Label::new(label.clone()).color(label_color).truncate())
.when(*icon_position == IconPosition::End, |flex| {
flex.child(
Icon::from_external_svg(custom_icon_svg.clone())
.size(*icon_size)
.color(icon_color),
)
})
.into_any_element()
} else if let Some(icon_name) = icon {
h_flex()
.gap_1p5()

View File

@@ -115,24 +115,24 @@ impl From<IconName> for Icon {
/// The source of an icon.
enum IconSource {
/// An SVG embedded in the Zed binary.
Svg(SharedString),
Embedded(SharedString),
/// An image file located at the specified path.
///
/// Currently our SVG renderer is missing support for the following features:
/// 1. Loading SVGs from external files.
/// 2. Rendering polychrome SVGs.
/// Currently our SVG renderer is missing support for rendering polychrome SVGs.
///
/// In order to support icon themes, we render the icons as images instead.
Image(Arc<Path>),
External(Arc<Path>),
/// An SVG not embedded in the Zed binary.
ExternalSvg(SharedString),
}
impl IconSource {
fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
if path.starts_with("icons/") {
Self::Svg(path)
Self::Embedded(path)
} else {
Self::Image(Arc::from(PathBuf::from(path.as_ref())))
Self::External(Arc::from(PathBuf::from(path.as_ref())))
}
}
}
@@ -148,7 +148,7 @@ pub struct Icon {
impl Icon {
pub fn new(icon: IconName) -> Self {
Self {
source: IconSource::Svg(icon.path().into()),
source: IconSource::Embedded(icon.path().into()),
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),
@@ -164,6 +164,15 @@ impl Icon {
}
}
pub fn from_external_svg(svg: SharedString) -> Self {
Self {
source: IconSource::ExternalSvg(svg),
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
@@ -193,14 +202,21 @@ impl Transformable for Icon {
impl RenderOnce for Icon {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
match self.source {
IconSource::Svg(path) => svg()
IconSource::Embedded(path) => svg()
.with_transformation(self.transformation)
.size(self.size)
.flex_none()
.path(path)
.text_color(self.color.color(cx))
.into_any_element(),
IconSource::Image(path) => img(path)
IconSource::ExternalSvg(path) => svg()
.external_path(path)
.with_transformation(self.transformation)
.size(self.size)
.flex_none()
.text_color(self.color.color(cx))
.into_any_element(),
IconSource::External(path) => img(path)
.size(self.size)
.flex_none()
.text_color(self.color.color(cx))

View File

@@ -136,124 +136,80 @@ async fn capture_windows(
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let shell_kind = ShellKind::new(shell_path, true);
let env_output = match shell_kind {
if let ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Xonsh = shell_kind
{
return Err(anyhow::anyhow!("unsupported shell kind"));
}
let mut cmd = crate::command::new_smol_command(shell_path);
let cmd = match shell_kind {
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Xonsh => {
return Err(anyhow::anyhow!("unsupported shell kind"));
unreachable!()
}
ShellKind::PowerShell => {
let output = crate::command::new_smol_command(shell_path)
.args([
"-NonInteractive",
"-NoProfile",
"-Command",
&format!(
"Set-Location '{}'; & '{}' --printenv",
directory.display(),
zed_path.display()
),
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
output
}
ShellKind::Elvish => {
let output = crate::command::new_smol_command(shell_path)
.args([
"-c",
&format!(
"cd '{}'; {} --printenv",
directory.display(),
zed_path.display()
),
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Elvish command failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
output
}
ShellKind::Nushell => {
let output = crate::command::new_smol_command(shell_path)
.args([
"-c",
&format!(
"cd '{}'; {}'{}' --printenv",
directory.display(),
shell_kind
.command_prefix()
.map(|prefix| prefix.to_string())
.unwrap_or_default(),
zed_path.display()
),
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
output
}
ShellKind::Cmd => {
let output = crate::command::new_smol_command(shell_path)
.args([
"/c",
&format!(
"cd '{}'; '{}' --printenv",
directory.display(),
zed_path.display()
),
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
output
}
};
let env_output = String::from_utf8_lossy(&env_output.stdout);
ShellKind::PowerShell => cmd.args([
"-NonInteractive",
"-NoProfile",
"-Command",
&format!(
"Set-Location '{}'; & '{}' --printenv",
directory.display(),
zed_path.display()
),
]),
ShellKind::Elvish => cmd.args([
"-c",
&format!(
"cd '{}'; {} --printenv",
directory.display(),
zed_path.display()
),
]),
ShellKind::Nushell => cmd.args([
"-c",
&format!(
"cd '{}'; {}'{}' --printenv",
directory.display(),
shell_kind
.command_prefix()
.map(|prefix| prefix.to_string())
.unwrap_or_default(),
zed_path.display()
),
]),
ShellKind::Cmd => cmd.args([
"/c",
"cd",
&directory.display().to_string(),
"&&",
&zed_path.display().to_string(),
"--printenv",
]),
}
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd
.output()
.await
.with_context(|| format!("command {cmd:?}"))?;
anyhow::ensure!(
output.status.success(),
"Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
// "cmd" "/c" "cd \'C:\\Workspace\\salsa\\\'; \'C:\\Workspace\\zed\\zed\\target\\debug\\zed.exe\' --printenv"
let env_output = String::from_utf8_lossy(&output.stdout);
// Parse the JSON output from zed --printenv
serde_json::from_str(&env_output)

View File

@@ -611,17 +611,21 @@ where
let file = caller.file().replace('\\', "/");
// In this codebase all crates reside in a `crates` directory,
// so discard the prefix up to that segment to find the crate name
let target = file
.split_once("crates/")
.and_then(|(_, s)| s.split_once("/src/"));
let file = file.split_once("crates/");
let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/"));
let module_path = target.map(|(krate, module)| {
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
if module.starts_with(krate) {
module.trim_end_matches(".rs").replace('/', "::")
} else {
krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::")
}
});
let file = file.map(|(_, file)| format!("crates/{file}"));
log::logger().log(
&log::Record::builder()
.target(target.map_or("", |(krate, _)| krate))
.module_path(module_path.as_deref())
.target(module_path.as_deref().unwrap_or(""))
.module_path(file.as_deref())
.args(format_args!("{:?}", error))
.file(Some(caller.file()))
.line(Some(caller.line()))

View File

@@ -791,12 +791,11 @@ impl WorkspaceDb {
remote_connection_id IS ?
LIMIT 1
})
.map(|mut prepared_statement| {
.and_then(|mut prepared_statement| {
(prepared_statement)((
root_paths.serialize().paths,
remote_connection_id.map(|id| id.0 as i32),
))
.unwrap()
})
.context("No workspaces found")
.warn_on_err()

View File

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

View File

@@ -1 +1 @@
dev
preview

View File

@@ -859,15 +859,19 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
// zed://settings/languages/Rust/tab_size - SUPPORT
// languages.$(language).tab_size
// [ languages $(language) tab_size]
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
match setting_path {
cx.spawn(async move |cx| {
let workspace =
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
workspace.update(cx, |_, window, cx| match setting_path {
None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx),
Some(setting_path) => window.dispatch_action(
Box::new(zed_actions::OpenSettingsAt { path: setting_path }),
cx,
),
}
});
})
})
.detach_and_log_err(cx);
}
}

View File

@@ -39,6 +39,7 @@ pub struct Record<'a> {
pub level: log::Level,
pub message: &'a std::fmt::Arguments<'a>,
pub module_path: Option<&'a str>,
pub line: Option<u32>,
}
pub fn init_output_stdout() {
@@ -105,7 +106,11 @@ static LEVEL_ANSI_COLORS: [&str; 6] = [
];
// PERF: batching
pub fn submit(record: Record) {
pub fn submit(mut record: Record) {
if record.module_path.is_none_or(|p| !p.ends_with(".rs")) {
// Only render line numbers for actual rust files emitted by `log_err` and friends
record.line.take();
}
if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) {
let mut stdout = std::io::stdout().lock();
_ = writeln!(
@@ -117,6 +122,7 @@ pub fn submit(record: Record) {
SourceFmt {
scope: record.scope,
module_path: record.module_path,
line: record.line,
ansi: true,
},
record.message
@@ -132,6 +138,7 @@ pub fn submit(record: Record) {
SourceFmt {
scope: record.scope,
module_path: record.module_path,
line: record.line,
ansi: true,
},
record.message
@@ -167,6 +174,7 @@ pub fn submit(record: Record) {
SourceFmt {
scope: record.scope,
module_path: record.module_path,
line: record.line,
ansi: false,
},
record.message
@@ -202,6 +210,7 @@ pub fn flush() {
struct SourceFmt<'a> {
scope: Scope,
module_path: Option<&'a str>,
line: Option<u32>,
ansi: bool,
}
@@ -225,6 +234,10 @@ impl std::fmt::Display for SourceFmt<'_> {
f.write_str(subscope)?;
}
}
if let Some(line) = self.line {
f.write_char(':')?;
line.fmt(f)?;
}
if self.ansi {
f.write_str(ANSI_RESET)?;
}

View File

@@ -80,7 +80,7 @@ impl log::Log for Zlog {
None => (private::scope_new(&[]), private::scope_new(&["*unknown*"])),
};
let level = record.metadata().level();
if !filter::is_scope_enabled(&crate_name_scope, record.module_path(), level) {
if !filter::is_scope_enabled(&crate_name_scope, Some(record.target()), level) {
return;
}
sink::submit(sink::Record {
@@ -89,6 +89,7 @@ impl log::Log for Zlog {
message: record.args(),
// PERF(batching): store non-static paths in a cache + leak them and pass static str here
module_path: record.module_path().or(record.file()),
line: record.line(),
});
}
@@ -109,6 +110,7 @@ macro_rules! log {
level,
message: &format_args!($($arg)+),
module_path: Some(module_path!()),
line: Some(line!()),
});
}
}
@@ -291,7 +293,7 @@ impl log::Log for Logger {
return;
}
let level = record.metadata().level();
if !filter::is_scope_enabled(&self.scope, record.module_path(), level) {
if !filter::is_scope_enabled(&self.scope, Some(record.target()), level) {
return;
}
sink::submit(sink::Record {
@@ -299,6 +301,7 @@ impl log::Log for Logger {
level,
message: record.args(),
module_path: record.module_path(),
line: record.line(),
});
}

View File

@@ -20,7 +20,7 @@ async function main() {
}
// currently we can only draft notes for patch releases.
if (parts[2] == 0) {
if (parts[2] === 0) {
process.exit(0);
}
@@ -41,20 +41,13 @@ async function main() {
"--depth",
100,
]);
execFileSync("git", [
"-C",
"target/shallow_clone",
"rev-parse",
"--verify",
tag,
]);
execFileSync("git", [
"-C",
"target/shallow_clone",
"rev-parse",
"--verify",
priorTag,
]);
execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", tag]);
try {
execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", priorTag]);
} catch (e) {
console.error(`Prior tag ${priorTag} not found`);
process.exit(0);
}
} catch (e) {
console.error(e.stderr.toString());
process.exit(1);
@@ -90,13 +83,7 @@ async function main() {
function getCommits(oldTag, newTag) {
const pullRequestNumbers = execFileSync(
"git",
[
"-C",
"target/shallow_clone",
"log",
`${oldTag}..${newTag}`,
"--format=DIVIDER\n%H|||%B",
],
["-C", "target/shallow_clone", "log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
{ encoding: "utf8" },
)
.replace(/\r\n/g, "\n")

View File

@@ -17,5 +17,6 @@ clap = { workspace = true, features = ["derive"] }
toml.workspace = true
indoc.workspace = true
indexmap.workspace = true
serde.workspace = true
toml_edit.workspace = true
gh-workflow.workspace = true

View File

@@ -3,6 +3,7 @@ use clap::Parser;
use std::fs;
use std::path::Path;
mod after_release;
mod cherry_pick;
mod compare_perf;
mod danger;
@@ -33,6 +34,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
("compare_perf.yml", compare_perf::compare_perf()),
("run_unit_evals.yml", run_agent_evals::run_unit_evals()),
("run_agent_evals.yml", run_agent_evals::run_agent_evals()),
("after_release.yml", after_release::after_release()),
];
fs::create_dir_all(dir)
.with_context(|| format!("Failed to create directory: {}", dir.display()))?;

View File

@@ -0,0 +1,123 @@
use gh_workflow::*;
use crate::tasks::workflows::{
runners,
steps::{NamedJob, dependant_job, named},
vars::{self, StepOutput},
};
pub fn after_release() -> Workflow {
let refresh_zed_dev = rebuild_releases_page();
let post_to_discord = post_to_discord(&[&refresh_zed_dev]);
let publish_winget = publish_winget();
named::workflow()
.on(Event::default().release(Release::default().types(vec![ReleaseType::Published])))
.add_job(refresh_zed_dev.name, refresh_zed_dev.job)
.add_job(post_to_discord.name, post_to_discord.job)
.add_job(publish_winget.name, publish_winget.job)
}
fn rebuild_releases_page() -> NamedJob {
named::job(
Job::default()
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.add_step(named::bash(
"curl https://zed.dev/api/revalidate-releases -H \"Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}\"",
).add_env(("RELEASE_NOTES_API_TOKEN", vars::RELEASE_NOTES_API_TOKEN))),
)
}
fn post_to_discord(deps: &[&NamedJob]) -> NamedJob {
fn get_release_url() -> Step<Run> {
named::bash(indoc::indoc! {r#"
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
"#})
.id("get-release-url")
}
fn get_content() -> Step<Use> {
named::uses(
"2428392",
"gh-truncate-string-action",
"b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1
)
.id("get-content")
.add_with((
"stringToTruncate",
indoc::indoc! {r#"
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
"#},
))
.add_with(("maxLength", 2000))
.add_with(("truncationSymbol", "..."))
}
fn discord_webhook_action() -> Step<Use> {
named::uses(
"tsickert",
"discord-webhook",
"c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0
)
.add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES))
.add_with(("content", "${{ steps.get-content.outputs.string }}"))
}
let job = dependant_job(deps)
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.add_step(get_release_url())
.add_step(get_content())
.add_step(discord_webhook_action());
named::job(job)
}
fn publish_winget() -> NamedJob {
fn set_package_name() -> (Step<Run>, StepOutput) {
let step = named::bash(indoc::indoc! {r#"
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
"#})
.id("set-package-name");
let output = StepOutput::new(&step, "PACKAGE_NAME");
(step, output)
}
fn winget_releaser(package_name: &StepOutput) -> Step<Use> {
named::uses(
"vedantmgoyal9",
"winget-releaser",
"19e706d4c9121098010096f9c495a70a7518b30f", // v2
)
.add_with(("identifier", package_name.to_string()))
.add_with(("max-versions-to-keep", 5))
.add_with(("token", vars::WINGET_TOKEN))
}
let (set_package_name, package_name) = set_package_name();
named::job(
Job::default()
.runs_on(runners::LINUX_SMALL)
.add_step(set_package_name)
.add_step(winget_releaser(&package_name)),
)
}

View File

@@ -36,6 +36,9 @@ secret!(ZED_SENTRY_MINIDUMP_ENDPOINT);
secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN);
secret!(ZED_ZIPPY_APP_ID);
secret!(ZED_ZIPPY_APP_PRIVATE_KEY);
secret!(DISCORD_WEBHOOK_RELEASE_NOTES);
secret!(WINGET_TOKEN);
secret!(RELEASE_NOTES_API_TOKEN);
// todo(ci) make these secrets too...
var!(AZURE_SIGNING_ACCOUNT_NAME);
@@ -136,6 +139,15 @@ impl StepOutput {
}
}
impl serde::Serialize for StepOutput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
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)
@@ -173,6 +185,15 @@ impl std::fmt::Display for Input {
}
}
impl serde::Serialize for Input {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
pub mod assets {
// NOTE: these asset names also exist in the zed.dev codebase.
pub const MAC_AARCH64: &str = "Zed-aarch64.dmg";