Compare commits

...

17 Commits

Author SHA1 Message Date
Zed Bot
d846d45519 Bump to 0.211.5 for @smitbarmase 2025-11-06 15:05:46 +00:00
Smit Barmase
f70f507f61 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:06 +05:30
Anthony Eid
e517719132 AI: Fix Github Copilot edit predictions failing to start (#41934)
Closes #41457 #41806 #41801

Copilot started using `node:sqlite` module which is an experimental
feature between node v22-v23 (stable in v24). The fix was passing in the
experimental flag when Zed starts the copilot LSP.

I tested this with v20.19.5 and v24.11.0. The fix got v20.19 working and
didn't affect v24.11 which was already working.

Release Notes:

- AI: Fix Github Copilot edit predictions failing to start
2025-11-06 13:30:28 +05:30
Richard Feldman
74efd3adbc 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:11:16 +01:00
Danilo Leal
9f1a9016b6 agent_ui: Fix how icons from external agents are displayed (#42034)
Release Notes:

- N/A
2025-11-06 08:07:59 +01:00
Danilo Leal
506f333ce1 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:06:59 +01:00
Finn Evers
214a0bc116 svg_preview: Update preview on every buffer edit (#41270)
Closes https://github.com/zed-industries/zed/issues/39104

This fixes an issue where the preview would not work for remote buffers
in the process.

Release Notes:

- Fixed an issue where the SVG preview would not work in remote
scenarios.
- The SVG preview will now rerender on every keypress instead of only on
saves.
2025-11-06 08:06:54 +01:00
Conrad Irwin
d461acbc7b Revert "Don't draft release notes"
This reverts commit 62ece18dfe.
2025-11-05 23:51:19 -07:00
zed-zippy[bot]
7acefd50cc Refresh zed.dev releases page after releases (#42060) (cherry-pick to stable) (#42064)
Cherry-pick of #42060 to stable

----
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-06 06:49:23 +00:00
Conrad Irwin
62ece18dfe Don't draft release notes 2025-11-05 10:45:11 -07:00
Joseph T. Lyons
4475689e4c v0.211.x stable 2025-11-05 12:07:10 -05:00
Ben Kunkle
5ea5b5e7a9 Fix integer underflow in autosave mode after delay in the settings (cherry-pick #41898) (#42013)
Closes #41774 

Release Notes:

- settings_ui: Fixed an integer underflow panic when attempting to hit
the `-` sign on settings item that take delays in milliseconds

Co-authored-by: Ignasius <96295999+ignasius-j-s@users.noreply.github.com>
2025-11-05 11:30:12 -05:00
Richard Feldman
d93f528a37 Use our node runtime for ACP extensions (#41955)
Release Notes:

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

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-05 12:24:23 +01:00
Richard Feldman
44140197a6 Add ACP terminal-login via _meta field (#41954)
As discussed with @benbrandt and @mikayla-maki:

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

Release Notes:

- Added experimental terminal-based authentication to ACP support
2025-11-05 11:40:11 +01:00
Lukas Wirth
2e746791b1 project: Fetch latest lsp data in deduplicate_range_based_lsp_requests 2025-11-05 09:24:05 +01:00
Lukas Wirth
a3f230f760 zed: Reduce number of rayon threads, spawn with bigger stacks (#41812)
We already do this for the cli and remote server but forgot to do so for
the main binary

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-05 09:13:16 +01:00
zed-zippy[bot]
ff0eef98c9 Fetch (just) enough refs in script/cherry-pick (#41949) (cherry-pick to preview) (#41951)
Cherry-pick of #41949 to preview

----
Before this change we'd download all the tagged commits, but none of
their ancestors,
this was slow and made cherry-picking fail.

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-04 19:45:56 -07:00
34 changed files with 1029 additions and 411 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

6
Cargo.lock generated
View File

@@ -16492,7 +16492,7 @@ dependencies = [
"editor",
"file_icons",
"gpui",
"multi_buffer",
"language",
"ui",
"workspace",
]
@@ -20928,6 +20928,7 @@ dependencies = [
"gh-workflow",
"indexmap 2.11.4",
"indoc",
"serde",
"toml 0.8.23",
"toml_edit 0.22.27",
]
@@ -21112,7 +21113,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.211.4"
version = "0.211.5"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21204,6 +21205,7 @@ dependencies = [
"project_symbols",
"prompt_store",
"proto",
"rayon",
"recent_projects",
"release_channel",
"remote",

View File

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

View File

@@ -1469,6 +1469,114 @@ 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();
// 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)),
full_label: label.to_string(),
label: label.to_string(),
command: Some(command.to_string()),
args,
command_label: label.to_string(),
cwd,
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 project = self.project.clone();
let authenticate = Self::spawn_external_agent_login(
login, workspace, project, false, true, 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
@@ -1567,7 +1675,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(()))
}
@@ -1617,17 +1728,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),
@@ -1645,44 +1779,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(())
})
}

View File

@@ -2040,7 +2040,7 @@ impl AgentPanel {
let mut entry =
ContextMenuEntry::new(format!("New {} Thread", 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);
}
@@ -2109,7 +2109,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

@@ -489,7 +489,11 @@ impl Copilot {
let node_path = node_runtime.binary_path().await?;
ensure_node_version_for_copilot(&node_path).await?;
let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
let arguments: Vec<OsString> = vec![
"--experimental-sqlite".into(),
server_path.into(),
"--stdio".into(),
];
let binary = LanguageServerBinary {
path: node_path,
arguments,

View File

@@ -164,6 +164,19 @@ 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"]
/// ```
///
/// 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

@@ -2,14 +2,13 @@ use crate::{
AnyElement, AnyImageCache, App, Asset, AssetLogger, Bounds, DefiniteLength, Element, ElementId,
Entity, GlobalElementId, Hitbox, Image, ImageCache, InspectorElementId, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SMOOTH_SVG_SCALE_FACTOR, SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task,
Window, px, swap_rgba_pa_to_bgra,
SharedString, SharedUri, StyleRefinement, Styled, Task, Window, px,
};
use anyhow::{Context as _, Result};
use futures::{AsyncReadExt, Future};
use image::{
AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba,
AnimationDecoder, DynamicImage, Frame, ImageError, ImageFormat, Rgba,
codecs::{gif::GifDecoder, webp::WebPDecoder},
};
use smallvec::SmallVec;
@@ -160,13 +159,15 @@ pub trait StyledImage: Sized {
self
}
/// Set the object fit for the image.
/// Set a fallback function that will be invoked to render an error view should
/// the image fail to load.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set the object fit for the image.
/// Set a fallback function that will be invoked to render a view while the image
/// is still being loaded.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
@@ -631,7 +632,7 @@ impl Asset for ImageAssetLoader {
}
};
let data = if let Ok(format) = image::guess_format(&bytes) {
if let Ok(format) = image::guess_format(&bytes) {
let data = match format {
ImageFormat::Gif => {
let decoder = GifDecoder::new(Cursor::new(&bytes))?;
@@ -689,25 +690,12 @@ impl Asset for ImageAssetLoader {
}
};
RenderImage::new(data)
Ok(Arc::new(RenderImage::new(data)))
} else {
let pixmap =
// TODO: Can we make svgs always rescale?
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
for pixel in buffer.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
image
};
Ok(Arc::new(data))
svg_renderer
.render_single_frame(&bytes, 1.0, true)
.map_err(Into::into)
}
}
}
}

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

@@ -95,7 +95,7 @@ pub use smol::Timer;
pub use style::*;
pub use styled::*;
pub use subscription::*;
use svg_renderer::*;
pub use svg_renderer::*;
pub(crate) use tab_stop::*;
pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]

View File

@@ -40,7 +40,7 @@ use crate::{
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window,
ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
@@ -1817,13 +1817,9 @@ impl Image {
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Svg => {
let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
let buffer =
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
.unwrap();
SmallVec::from_elem(Frame::new(buffer), 1)
return svg_renderer
.render_single_frame(&self.bytes, 1.0, false)
.map_err(Into::into);
}
};

View File

@@ -1,5 +1,10 @@
use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
use crate::{
AssetSource, DevicePixels, IsZero, RenderImage, Result, SharedString, Size,
swap_rgba_pa_to_bgra,
};
use image::Frame;
use resvg::tiny_skia::Pixmap;
use smallvec::SmallVec;
use std::{
hash::Hash,
sync::{Arc, LazyLock},
@@ -15,17 +20,22 @@ pub(crate) struct RenderSvgParams {
}
#[derive(Clone)]
/// A struct holding everything necessary to render SVGs.
pub struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
usvg_options: Arc<usvg::Options<'static>>,
}
/// The size in which to render the SVG.
pub enum SvgSize {
/// An absolute size in device pixels.
Size(Size<DevicePixels>),
/// A scaling factor to apply to the size provided by the SVG.
ScaleFactor(f32),
}
impl SvgRenderer {
/// Creates a new SVG renderer with the provided asset source.
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
let mut db = usvg::fontdb::Database::new();
@@ -54,33 +64,68 @@ impl SvgRenderer {
}
}
pub(crate) fn render(
/// Renders the given bytes into an image buffer.
pub fn render_single_frame(
&self,
bytes: &[u8],
scale_factor: f32,
to_brga: bool,
) -> Result<Arc<RenderImage>, usvg::Error> {
self.render_pixmap(
bytes,
SvgSize::ScaleFactor(scale_factor * SMOOTH_SVG_SCALE_FACTOR),
)
.map(|pixmap| {
let mut buffer =
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
.unwrap();
if to_brga {
for pixel in buffer.chunks_exact_mut(4) {
swap_rgba_pa_to_bgra(pixel);
}
}
let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)]));
image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
Arc::new(image)
})
}
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)
}
}
pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
let svg_size = tree.size();
let scale = match size {

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(&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(),
))
@@ -1466,6 +1468,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")?,
@@ -1476,6 +1479,7 @@ impl LspAdapter for PyLspAdapter {
};
Some(language::CodeLabel::filtered(
label.clone(),
label_len,
item.filter_text.as_deref(),
vec![(0..label.len(), highlight_id)],
))
@@ -1741,6 +1745,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"),
@@ -1762,6 +1767,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

@@ -267,6 +267,7 @@ impl AgentServerStore {
// Insert agent servers from extension manifests
match &self.state {
AgentServerStoreState::Local {
node_runtime,
project_environment,
fs,
http_client,
@@ -297,6 +298,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(),
@@ -444,6 +446,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>,
@@ -1364,6 +1373,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>,
@@ -1387,6 +1397,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();
@@ -1534,23 +1545,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,
@@ -1558,7 +1575,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))
})
}
@@ -1829,6 +1846,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,4 +1911,85 @@ mod extension_agent_tests {
let target = manifest_entry.targets.get("linux-x86_64").unwrap();
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 project_environment = cx.new(|cx| crate::ProjectEnvironment::new(None, cx));
let agent = LocalExtensionArchiveAgent {
fs,
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"]);
}
#[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 project_environment = cx.new(|cx| crate::ProjectEnvironment::new(None, cx));
let agent = LocalExtensionArchiveAgent {
fs,
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
}
}

View File

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

View File

@@ -15,6 +15,6 @@ path = "src/svg_preview.rs"
editor.workspace = true
file_icons.workspace = true
gpui.workspace = true
multi_buffer.workspace = true
language.workspace = true
ui.workspace = true
workspace.workspace = true

View File

@@ -1,13 +1,13 @@
use std::path::PathBuf;
use std::mem;
use std::sync::Arc;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, IntoElement,
ParentElement, Render, Resource, RetainAllImageCache, Styled, Subscription, WeakEntity, Window,
div, img,
App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img,
};
use multi_buffer::{Event as MultiBufferEvent, MultiBuffer};
use language::{Buffer, BufferEvent};
use ui::prelude::*;
use workspace::item::Item;
use workspace::{Pane, Workspace};
@@ -16,9 +16,10 @@ use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
pub struct SvgPreviewView {
focus_handle: FocusHandle,
svg_path: Option<PathBuf>,
image_cache: Entity<RetainAllImageCache>,
_buffer_subscription: Subscription,
buffer: Option<Entity<Buffer>>,
current_svg: Option<Result<Arc<RenderImage>, SharedString>>,
_refresh: Task<()>,
_buffer_subscription: Option<Subscription>,
_workspace_subscription: Option<Subscription>,
}
@@ -31,6 +32,182 @@ pub enum SvgPreviewMode {
}
impl SvgPreviewView {
pub fn new(
mode: SvgPreviewMode,
active_editor: Entity<Editor>,
workspace_handle: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
cx.new(|cx| {
let workspace_subscription = if mode == SvgPreviewMode::Follow
&& let Some(workspace) = workspace_handle.upgrade()
{
Some(Self::subscribe_to_workspace(workspace, window, cx))
} else {
None
};
let buffer = active_editor
.read(cx)
.buffer()
.clone()
.read_with(cx, |buffer, _cx| buffer.as_singleton());
let subscription = buffer
.as_ref()
.map(|buffer| Self::create_buffer_subscription(buffer, window, cx));
let mut this = Self {
focus_handle: cx.focus_handle(),
buffer,
current_svg: None,
_buffer_subscription: subscription,
_workspace_subscription: workspace_subscription,
_refresh: Task::ready(()),
};
this.render_image(window, cx);
this
})
}
fn subscribe_to_workspace(
workspace: Entity<Workspace>,
window: &Window,
cx: &mut Context<Self>,
) -> Subscription {
cx.subscribe_in(
&workspace,
window,
move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
if let workspace::Event::ActiveItemChanged = event {
let workspace = workspace.read(cx);
if let Some(active_item) = workspace.active_item(cx)
&& let Some(editor) = active_item.downcast::<Editor>()
&& Self::is_svg_file(&editor, cx)
{
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
return;
};
if this.buffer.as_ref() != Some(&buffer) {
this._buffer_subscription =
Some(Self::create_buffer_subscription(&buffer, window, cx));
this.buffer = Some(buffer);
this.render_image(window, cx);
cx.notify();
}
} else {
this.set_current(None, window, cx);
}
}
},
)
}
fn render_image(&mut self, window: &Window, cx: &mut Context<Self>) {
let Some(buffer) = self.buffer.as_ref() else {
return;
};
const SCALE_FACTOR: f32 = 1.0;
let renderer = cx.svg_renderer();
let content = buffer.read(cx).snapshot();
let background_task = cx.background_spawn(async move {
renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true)
});
self._refresh = cx.spawn_in(window, async move |this, cx| {
let result = background_task.await;
this.update_in(cx, |view, window, cx| {
let current = result.map_err(|e| e.to_string().into());
view.set_current(Some(current), window, cx);
})
.ok();
});
}
fn set_current(
&mut self,
image: Option<Result<Arc<RenderImage>, SharedString>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) {
window.drop_image(image).ok();
}
cx.notify();
}
fn find_existing_preview_item_idx(
pane: &Pane,
editor: &Entity<Editor>,
cx: &App,
) -> Option<usize> {
let buffer_id = editor.read(cx).buffer().entity_id();
pane.items_of_type::<SvgPreviewView>()
.find(|view| {
view.read(cx)
.buffer
.as_ref()
.is_some_and(|buffer| buffer.entity_id() == buffer_id)
})
.and_then(|view| pane.index_for_item(&view))
}
pub fn resolve_active_item_as_svg_editor(
workspace: &Workspace,
cx: &mut Context<Workspace>,
) -> Option<Entity<Editor>> {
workspace
.active_item(cx)?
.act_as::<Editor>(cx)
.filter(|editor| Self::is_svg_file(&editor, cx))
}
fn create_svg_view(
mode: SvgPreviewMode,
workspace: &mut Workspace,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<SvgPreviewView> {
let workspace_handle = workspace.weak_handle();
SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
}
fn create_buffer_subscription(
buffer: &Entity<Buffer>,
window: &Window,
cx: &mut Context<Self>,
) -> Subscription {
cx.subscribe_in(
buffer,
window,
move |this, _buffer, event: &BufferEvent, window, cx| match event {
BufferEvent::Edited | BufferEvent::Saved => {
this.render_image(window, cx);
}
_ => {}
},
)
}
pub fn is_svg_file(editor: &Entity<Editor>, cx: &App) -> bool {
editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
.is_some_and(|file| {
file.path()
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
})
}
pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx)
@@ -104,154 +281,6 @@ impl SvgPreviewView {
}
});
}
fn find_existing_preview_item_idx(
pane: &Pane,
editor: &Entity<Editor>,
cx: &App,
) -> Option<usize> {
let editor_path = Self::get_svg_path(editor.read(cx).buffer(), cx);
pane.items_of_type::<SvgPreviewView>()
.find(|view| {
let view_read = view.read(cx);
view_read.svg_path.is_some() && view_read.svg_path == editor_path
})
.and_then(|view| pane.index_for_item(&view))
}
pub fn resolve_active_item_as_svg_editor(
workspace: &Workspace,
cx: &mut Context<Workspace>,
) -> Option<Entity<Editor>> {
let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
if Self::is_svg_file(&editor, cx) {
Some(editor)
} else {
None
}
}
fn create_svg_view(
mode: SvgPreviewMode,
workspace: &mut Workspace,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<SvgPreviewView> {
let workspace_handle = workspace.weak_handle();
SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
}
pub fn new(
mode: SvgPreviewMode,
active_editor: Entity<Editor>,
workspace_handle: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
cx.new(|cx| {
let image_cache = RetainAllImageCache::new(cx);
let buffer = active_editor.read(cx).buffer();
let svg_path = Self::get_svg_path(buffer, cx);
let subscription = Self::create_buffer_subscription(&buffer.clone(), window, cx);
// Subscribe to workspace active item changes to follow SVG files
let workspace_subscription = if mode == SvgPreviewMode::Follow {
workspace_handle.upgrade().map(|workspace_handle| {
cx.subscribe_in(
&workspace_handle,
window,
|this: &mut SvgPreviewView,
workspace,
event: &workspace::Event,
window,
cx| {
if let workspace::Event::ActiveItemChanged = event {
let workspace_read = workspace.read(cx);
if let Some(active_item) = workspace_read.active_item(cx)
&& let Some(editor) = active_item.downcast::<Editor>()
&& Self::is_svg_file(&editor, cx)
{
let buffer = editor.read(cx).buffer();
let new_path = Self::get_svg_path(&buffer, cx);
if this.svg_path != new_path {
this.svg_path = new_path;
this._buffer_subscription =
Self::create_buffer_subscription(
&buffer.clone(),
window,
cx,
);
cx.notify();
}
}
}
},
)
})
} else {
None
};
Self {
focus_handle: cx.focus_handle(),
svg_path,
image_cache,
_buffer_subscription: subscription,
_workspace_subscription: workspace_subscription,
}
})
}
fn create_buffer_subscription(
active_buffer: &Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Subscription {
cx.subscribe_in(
active_buffer,
window,
|this: &mut SvgPreviewView, buffer, event: &MultiBufferEvent, window, cx| {
let potential_path_change = event == &MultiBufferEvent::FileHandleChanged;
if event == &MultiBufferEvent::Saved || potential_path_change {
// Remove cached image to force reload
if let Some(svg_path) = &this.svg_path {
let resource = Resource::Path(svg_path.clone().into());
this.image_cache.update(cx, |cache, cx| {
cache.remove(&resource, window, cx);
});
}
if potential_path_change {
this.svg_path = Self::get_svg_path(buffer, cx);
}
cx.notify();
}
},
)
}
pub fn is_svg_file(editor: &Entity<Editor>, cx: &App) -> bool {
let buffer = editor.read(cx).buffer().read(cx);
if let Some(buffer) = buffer.as_singleton()
&& let Some(file) = buffer.read(cx).file()
{
return file
.path()
.extension()
.map(|ext| ext.eq_ignore_ascii_case("svg"))
.unwrap_or(false);
}
false
}
fn get_svg_path(buffer: &Entity<MultiBuffer>, cx: &App) -> Option<PathBuf> {
let buffer = buffer.read(cx).as_singleton()?;
let file = buffer.read(cx).file()?;
let local_file = file.as_local()?;
Some(local_file.abs_path(cx))
}
}
impl Render for SvgPreviewView {
@@ -265,20 +294,19 @@ impl Render for SvgPreviewView {
.flex()
.justify_center()
.items_center()
.child(if let Some(svg_path) = &self.svg_path {
img(ImageSource::from(svg_path.clone()))
.image_cache(&self.image_cache)
.max_w_full()
.max_h_full()
.with_fallback(|| {
div()
.map(|this| match self.current_svg.clone() {
Some(Ok(image)) => {
this.child(img(image).max_w_full().max_h_full().with_fallback(|| {
h_flex()
.p_4()
.child("Failed to load SVG file")
.gap_2()
.child(Icon::new(IconName::Warning))
.child("Failed to load SVG image")
.into_any_element()
})
.into_any_element()
} else {
div().p_4().child("No SVG file selected").into_any_element()
}))
}
Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()),
None => this.child(div().p_4().child("No SVG file selected")),
})
}
}
@@ -295,20 +323,19 @@ impl Item for SvgPreviewView {
type Event = ();
fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
// Use the same icon as SVG files in the file tree
self.svg_path
self.buffer
.as_ref()
.and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
.and_then(|buffer| buffer.read(cx).file())
.and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx))
.map(Icon::from_path)
.or_else(|| Some(Icon::new(IconName::Image)))
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
self.svg_path
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
self.buffer
.as_ref()
.and_then(|svg_path| svg_path.file_name())
.map(|name| name.to_string_lossy())
.map(|name| format!("Preview {}", name).into())
.and_then(|svg_path| svg_path.read(cx).file())
.map(|name| format!("Preview {}", name.file_name(cx)).into())
.unwrap_or_else(|| "SVG Preview".into())
}

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

@@ -31,7 +31,7 @@ pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr
fn saturating_sub(self, rhs: Self) -> Self;
}
macro_rules! impl_newtype_numeric_stepper {
macro_rules! impl_newtype_numeric_stepper_float {
($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
impl NumberFieldType for $type {
fn default_step() -> Self {
@@ -65,13 +65,47 @@ macro_rules! impl_newtype_numeric_stepper {
};
}
macro_rules! impl_newtype_numeric_stepper_int {
($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => {
impl NumberFieldType for $type {
fn default_step() -> Self {
$default.into()
}
fn large_step() -> Self {
$large.into()
}
fn small_step() -> Self {
$small.into()
}
fn min_value() -> Self {
$min.into()
}
fn max_value() -> Self {
$max.into()
}
fn saturating_add(self, rhs: Self) -> Self {
$type(self.0.saturating_add(rhs.0).min(Self::max_value().0))
}
fn saturating_sub(self, rhs: Self) -> Self {
$type(self.0.saturating_sub(rhs.0).max(Self::min_value().0))
}
}
};
}
#[rustfmt::skip]
impl_newtype_numeric_stepper!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000);
impl_newtype_numeric_stepper!(
impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK);
impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9);
impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0);
impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0);
impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000);
impl_newtype_numeric_stepper_float!(
CenteredPaddingSettings,
0.05,
0.2,

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.211.4"
version = "0.211.5"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -73,6 +73,7 @@ gpui = { workspace = true, features = [
"windows-manifest",
] }
gpui_tokio.workspace = true
rayon.workspace = true
edit_prediction_button.workspace = true
http_client.workspace = true

View File

@@ -1 +1 @@
preview
stable

View File

@@ -257,6 +257,13 @@ pub fn main() {
return;
}
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.stack_size(10 * 1024 * 1024)
.thread_name(|ix| format!("RayonWorker{}", ix))
.build_global()
.unwrap();
log::info!(
"========== starting zed version {}, sha {} ==========",
app_version,

View File

@@ -41,6 +41,9 @@ const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[
("blade_graphics", log::LevelFilter::Warn),
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))]
("naga::back::spv::writer", log::LevelFilter::Warn),
// usvg prints a lot of warnings on rendering an SVG with partial errors, which
// can happen a lot with the SVG preview
("usvg::parser::style", log::LevelFilter::Error),
];
pub fn init_env_filter(filter: env_config::EnvFilter) {

View File

@@ -12,9 +12,8 @@ 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"

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";