Compare commits

...

39 Commits

Author SHA1 Message Date
Jakub Konka
637296ce6e git_panel: Removing offending notify 2025-11-13 17:15:12 +01:00
Jakub Konka
90710b91c5 git_panel: Clean up a little 2025-11-12 23:14:18 +01:00
Jakub Konka
53f1fd5a27 git_panel: Debugging flickers 2025-11-12 19:21:05 +01:00
Joseph T. Lyons
e98de8c7ca v0.212.x stable 2025-11-12 10:15:09 -05:00
zed-zippy[bot]
fbd98c1fdc agent_servers: Fix panic when setting default mode (#42452) (cherry-pick to preview) (#42455)
Cherry-pick of #42452 to preview

----
Closes ZED-35A

Release Notes:

- Fixed an issue where Zed would panic when trying to set the default
mode for ACP agents

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-11-11 17:12:44 +01:00
zed-zippy[bot]
50b491b360 gpui: Do not panic when unable to find the selected fonts (#42212) (cherry-pick to preview) (#42456)
Cherry-pick of #42212 to preview

----
Fixes ZED-329

Release Notes:

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

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-11 16:44:51 +01:00
zed-zippy[bot]
9dab0e5f9b remote: Add more context to error logging in wsl (#42450) (cherry-pick to preview) (#42451)
Cherry-pick of #42450 to preview

----
cc https://github.com/zed-industries/zed/issues/40892

Release Notes:

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

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-11 15:20:50 +00:00
zed-zippy[bot]
b0c8952b0c gpui: Fix invalid unwrap in windows window creation (#42426) (cherry-pick to preview) (#42429)
Cherry-pick of #42426 to preview

----
Fixes ZED-34M

Release Notes:

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

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-11 12:18:48 +01:00
zed-zippy[bot]
47bdf9f026 terminal: Spawn terminal process on main thread on macos again (#42411) (cherry-pick to preview) (#42413)
Cherry-pick of #42411 to preview

----
Closes https://github.com/zed-industries/zed/issues/42365, follow up to
https://github.com/zed-industries/zed/pull/42234

Release Notes:

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

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-11 11:57:24 +01:00
zed-zippy[bot]
02cc7765f9 diagnostics: Fix panic due non-sorted diagnostics excerpt ranges (#42416) (cherry-pick to preview) (#42419)
Cherry-pick of #42416 to preview

----
Fixes ZED-356

Release Notes:

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

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-11 10:36:06 +00:00
zed-zippy[bot]
07ffb17991 remote: Flush to stdin when writing to sftp (#42103) (cherry-pick to preview) (#42361)
Cherry-pick of #42103 to preview

----

https://github.com/zed-industries/zed/issues/42027#issuecomment-3497210172

Release Notes:

- Fixed ssh remoting potentially failing due to not flushing stdin to
sftp

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-10 21:34:08 +00:00
Conrad Irwin
b8c644a12e Create sentry releases in after_release (#42169)
This had been moved to auto-release preview, and was not running for
stable.

Closes #ISSUE

Release Notes:

- N/A
2025-11-10 10:17:13 -07:00
Zed Bot
f7dac91680 Bump to 0.212.3 for @bennetbo 2025-11-10 16:35:26 +00:00
zed-zippy[bot]
5a8a80a956 diagnostics: Keep diagnostic excerpt ranges properly ordered (#42298) (cherry-pick to preview) (#42300)
Cherry-pick of #42298 to preview

----
Fixes ZED-2CQ

We were doing the binary search by buffer points, but due to await
points within this function we could end up mixing points of differing
buffer versions.

Release Notes:

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

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-10 14:37:03 +00:00
zed-zippy[bot]
0c64f5185f acp: Fix issue with mentions when embedded_context is set to false (#42260) (cherry-pick to preview) (#42320)
Cherry-pick of #42260 to preview

----
Release Notes:

- acp: Fixed an issue where Zed would not respect
`PromptCapabilities::embedded_context`

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-11-10 09:14:19 +00:00
zed-zippy[bot]
56bf1e09f7 agent_ui: Always allow to include symbols (#42261) (cherry-pick to preview) (#42321)
Cherry-pick of #42261 to preview

----
We can always include symbols, since we either include a ResourceLink to
the symbol (when `PromptCapabilities::embedded_context = false`) or a
Resource (when `PromptCapabilities::embedded_context = true`)

Release Notes:

- Fixed an issue where symbols could not be included when using specific
ACP agents

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-11-10 08:16:21 +00:00
zed-zippy[bot]
1ce5974917 Fix crash during drag-and-drop on Windows (#42227) (cherry-pick to preview) (#42301)
Cherry-pick of #42227 to preview

----
The HGLOBAL is itself the HDROP. Do not dereference it.

Release Notes:

- windows: Fixed crashes during drag-and-drop operations

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-09 11:29:39 +00:00
zed-zippy[bot]
db7d11e879 terminal: Spawn terminal process on main thread on unix (#42234) (cherry-pick to preview) (#42236)
Cherry-pick of #42234 to preview

----
Otherwise the terminal will not process the signals correctly 

Release Notes:

- Fixed ctrl+c and friends not working in the terminal on macOS and
linux

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 21:08:56 +00:00
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
54 changed files with 1277 additions and 763 deletions

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

@@ -0,0 +1,74 @@
# Generated from xtask::workflows::after_release
# Rebuild with `cargo xtask workflows`.
name: after_release
on:
release:
types:
- published
jobs:
post_to_discord:
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 }}
create_sentry_release:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_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

View File

@@ -475,14 +475,6 @@ jobs:
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

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.3"
dependencies = [
"acp_tools",
"activity_indicator",

View File

@@ -50,13 +50,14 @@ impl crate::AgentServer for CustomAgentServer {
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
settings
if let Some(settings) = settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
.unwrap()
.default_mode = mode_id.map(|m| m.to_string())
{
settings.default_mode = mode_id.map(|m| m.to_string())
}
});
}

View File

@@ -646,16 +646,14 @@ impl ContextPickerCompletionProvider {
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
let mut entries = if embedded_context {
vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
ContextPickerEntry::Mode(ContextPickerMode::Thread),
]
} else {
// File is always available, but we don't need a mode entry
vec![]
};
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
if embedded_context {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
}
let has_selection = workspace
.read(cx)

View File

@@ -356,7 +356,7 @@ impl MessageEditor {
let task = match mention_uri.clone() {
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
@@ -373,7 +373,6 @@ impl MessageEditor {
)))
}
MentionUri::Selection { .. } => {
// Handled elsewhere
debug_panic!("unexpected selection URI");
Task::ready(Err(anyhow!("unexpected selection URI")))
}
@@ -704,13 +703,11 @@ impl MessageEditor {
return Task::ready(Err(err));
}
let contents = self.mention_set.contents(
&self.prompt_capabilities.borrow(),
full_mention_content,
self.project.clone(),
cx,
);
let contents = self
.mention_set
.contents(full_mention_content, self.project.clone(), cx);
let editor = self.editor.clone();
let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
cx.spawn(async move |_, cx| {
let contents = contents.await?;
@@ -738,18 +735,32 @@ impl MessageEditor {
tracked_buffers,
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
if supports_embedded_context {
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource:
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
} else {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
}
Mention::Image(mention_image) => {
let uri = match uri {
@@ -771,18 +782,16 @@ impl MessageEditor {
meta: None,
})
}
Mention::UriOnly => {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
};
chunks.push(chunk);
ix = crease_range.end;
@@ -1111,7 +1120,7 @@ impl MessageEditor {
let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len();
mentions.push((start..end, mention_uri, Mention::UriOnly));
mentions.push((start..end, mention_uri, Mention::Link));
}
}
acp::ContentBlock::Image(acp::ImageContent {
@@ -1517,7 +1526,7 @@ pub enum Mention {
tracked_buffers: Vec<Entity<Buffer>>,
},
Image(MentionImage),
UriOnly,
Link,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1534,21 +1543,10 @@ pub struct MentionSet {
impl MentionSet {
fn contents(
&self,
prompt_capabilities: &acp::PromptCapabilities,
full_mention_content: bool,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
if !prompt_capabilities.embedded_context {
let mentions = self
.mentions
.iter()
.map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
.collect();
return Task::ready(Ok(mentions));
}
let mentions = self.mentions.clone();
cx.spawn(async move |cx| {
let mut contents = HashMap::default();
@@ -2202,6 +2200,8 @@ mod tests {
format!("seven.txt b{slash}"),
format!("six.txt b{slash}"),
format!("five.txt b{slash}"),
"Files & Directories".into(),
"Symbols".into()
]
);
editor.set_text("", window, cx);
@@ -2286,21 +2286,11 @@ mod tests {
assert_eq!(fold_ranges(editor, cx).len(), 1);
});
let all_prompt_capabilities = acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
};
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2318,30 +2308,6 @@ mod tests {
);
}
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&acp::PromptCapabilities::default(),
false,
project.clone(),
cx,
)
})
.await
.unwrap()
.into_values()
.collect::<Vec<_>>();
{
let [(uri, Mention::UriOnly)] = contents.as_slice() else {
panic!("Unexpected mentions");
};
pretty_assertions::assert_eq!(
uri,
&MentionUri::parse(&url_one, PathStyle::local()).unwrap()
);
}
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
@@ -2377,12 +2343,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2503,12 +2466,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2554,12 +2514,9 @@ mod tests {
// Getting the message contents fails
message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.expect_err("Should fail to load x.png");
@@ -2610,12 +2567,9 @@ mod tests {
// Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap();
@@ -2897,6 +2851,147 @@ mod tests {
);
}
#[gpui::test]
async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
fs.insert_tree(
"/project",
json!({
"src": {
"main.rs": file_content,
}
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.clone(),
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
},
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});
cx.simulate_input("What is in @file main");
editor.update_in(cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
assert_eq!(editor.text(cx), "What is in @file main");
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
let main_rs_uri = if cfg!(windows) {
"file:///C:/project/src/main.rs".to_string()
} else {
"file:///project/src/main.rs".to_string()
};
// When embedded context is `false` we should get a resource link
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: main_rs_uri.clone(),
name: "main.rs".to_string(),
annotations: None,
meta: None,
description: None,
mime_type: None,
size: None,
title: None,
})
]
);
message_editor.update(cx, |editor, _cx| {
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
..Default::default()
})
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
// When embedded context is `true` we should get a resource
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
text: file_content.to_string(),
uri: main_rs_uri,
mime_type: None,
meta: None
}
),
annotations: None,
meta: None
})
]
);
}
#[gpui::test]
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
init_test(cx);

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

@@ -23,7 +23,7 @@ use project::{
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp::Ordering,
cmp::{self, Ordering},
sync::Arc,
};
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
@@ -410,7 +410,7 @@ impl BufferDiagnosticsEditor {
// in the editor.
// This is done by iterating over the list of diagnostic blocks and
// determine what range does the diagnostic block span.
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
let mut excerpt_ranges: Vec<ExcerptRange<_>> = Vec::new();
for diagnostic_block in blocks.iter() {
let excerpt_range = context_range_for_entry(
@@ -420,30 +420,43 @@ impl BufferDiagnosticsEditor {
&mut cx,
)
.await;
let initial_range = buffer_snapshot
.anchor_after(diagnostic_block.initial_range.start)
..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);
let index = excerpt_ranges
.binary_search_by(|probe| {
let bin_search = |probe: &ExcerptRange<text::Anchor>| {
let context_start = || {
probe
.context
.start
.cmp(&excerpt_range.start)
.then(probe.context.end.cmp(&excerpt_range.end))
.then(
probe
.primary
.start
.cmp(&diagnostic_block.initial_range.start),
)
.then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|index| index);
.cmp(&excerpt_range.start, &buffer_snapshot)
};
let context_end =
|| probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
let primary_start = || {
probe
.primary
.start
.cmp(&initial_range.start, &buffer_snapshot)
};
let primary_end =
|| probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
context_start()
.then_with(context_end)
.then_with(primary_start)
.then_with(primary_end)
.then(cmp::Ordering::Greater)
};
let index = excerpt_ranges
.binary_search_by(bin_search)
.unwrap_or_else(|i| i);
excerpt_ranges.insert(
index,
ExcerptRange {
context: excerpt_range,
primary: diagnostic_block.initial_range.clone(),
primary: initial_range,
},
)
}
@@ -466,6 +479,13 @@ impl BufferDiagnosticsEditor {
buffer_diagnostics_editor
.multibuffer
.update(cx, |multibuffer, cx| {
let excerpt_ranges = excerpt_ranges
.into_iter()
.map(|range| ExcerptRange {
context: range.context.to_point(&buffer_snapshot),
primary: range.primary.to_point(&buffer_snapshot),
})
.collect();
multibuffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),

View File

@@ -39,8 +39,8 @@ impl DiagnosticRenderer {
let group_id = primary.diagnostic.group_id;
let mut results = vec![];
for entry in diagnostic_group.iter() {
let mut markdown = Self::markdown(&entry.diagnostic);
if entry.diagnostic.is_primary {
let mut markdown = Self::markdown(&entry.diagnostic);
let diagnostic = &primary.diagnostic;
if diagnostic.source.is_some() || diagnostic.code.is_some() {
markdown.push_str(" (");
@@ -81,21 +81,12 @@ impl DiagnosticRenderer {
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
let markdown = Self::markdown(&entry.diagnostic);
results.push(DiagnosticBlock {
initial_range: entry.range.clone(),
severity: entry.diagnostic.severity,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
});
} else {
let mut markdown = Self::markdown(&entry.diagnostic);
markdown.push_str(&format!(
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
));
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
markdown.push_str(&format!(
" ([back](file://#diagnostic-{buffer_id}-{group_id}-{primary_ix}))"
));
}
results.push(DiagnosticBlock {
initial_range: entry.range.clone(),
severity: entry.diagnostic.severity,

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(
@@ -511,7 +494,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let mut buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
@@ -559,6 +542,7 @@ impl ProjectDiagnosticsEditor {
}
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
let diagnostics_toolbar_editor = Arc::new(this.clone());
for (_, group) in grouped {
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
if group_severity.is_none_or(|s| s > max_severity) {
@@ -568,7 +552,7 @@ impl ProjectDiagnosticsEditor {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(Arc::new(this.clone())),
Some(diagnostics_toolbar_editor.clone()),
cx,
)
})?;
@@ -576,21 +560,37 @@ 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 cmp_excerpts = |buffer_snapshot: &BufferSnapshot,
a: &ExcerptRange<text::Anchor>,
b: &ExcerptRange<text::Anchor>| {
let context_start = || a.context.start.cmp(&b.context.start, buffer_snapshot);
let context_end = || a.context.end.cmp(&b.context.end, buffer_snapshot);
let primary_start = || a.primary.start.cmp(&b.primary.start, buffer_snapshot);
let primary_end = || a.primary.end.cmp(&b.primary.end, buffer_snapshot);
context_start()
.then_with(context_end)
.then_with(primary_start)
.then_with(primary_end)
.then(cmp::Ordering::Greater)
};
let mut excerpt_ranges: Vec<ExcerptRange<_>> = 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()
})
})?,
};
.map(|(_, range)| range)
.sorted_by(|a, b| cmp_excerpts(&buffer_snapshot, a, b))
.collect(),
}
})
})?;
let mut result_blocks = vec![None; excerpt_ranges.len()];
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks {
@@ -601,26 +601,17 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
..buffer_snapshot.anchor_before(b.initial_range.end);
let excerpt_range = ExcerptRange {
context: excerpt_range,
primary: initial_range,
};
let i = excerpt_ranges
.binary_search_by(|probe| {
probe
.context
.start
.cmp(&excerpt_range.start)
.then(probe.context.end.cmp(&excerpt_range.end))
.then(probe.primary.start.cmp(&b.initial_range.start))
.then(probe.primary.end.cmp(&b.initial_range.end))
.then(cmp::Ordering::Greater)
})
.binary_search_by(|probe| cmp_excerpts(&buffer_snapshot, probe, &excerpt_range))
.unwrap_or_else(|i| i);
excerpt_ranges.insert(
i,
ExcerptRange {
context: excerpt_range,
primary: b.initial_range.clone(),
},
);
excerpt_ranges.insert(i, excerpt_range);
result_blocks.insert(i, Some(b));
}
@@ -633,6 +624,13 @@ impl ProjectDiagnosticsEditor {
})
}
let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
let excerpt_ranges = excerpt_ranges
.into_iter()
.map(|range| ExcerptRange {
context: range.context.to_point(&buffer_snapshot),
primary: range.primary.to_point(&buffer_snapshot),
})
.collect();
multi_buffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
@@ -946,7 +944,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);
});
}
@@ -978,8 +976,8 @@ async fn context_range_for_entry(
context: u32,
snapshot: BufferSnapshot,
cx: &mut AsyncApp,
) -> Range<Point> {
if let Some(rows) = heuristic_syntactic_expand(
) -> Range<text::Anchor> {
let range = if let Some(rows) = heuristic_syntactic_expand(
range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot.clone(),
@@ -987,15 +985,17 @@ async fn context_range_for_entry(
)
.await
{
return Range {
Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
Range {
start: Point::new(range.start.row.saturating_sub(context), 0),
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
}
}
} else {
Range {
start: Point::new(range.start.row.saturating_sub(context), 0),
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
}
};
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end)
}
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited

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

@@ -24755,7 +24755,7 @@ fn render_diff_hunk_controls(
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
_window: &mut Window,
_win: &mut Window,
cx: &mut App,
) -> AnyElement {
h_flex()
@@ -24777,7 +24777,7 @@ fn render_diff_hunk_controls(
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |_window, cx| {
move |_win, cx| {
Tooltip::for_action_in(
"Stage Hunk",
&::git::ToggleStaged,

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

@@ -216,7 +216,7 @@ struct GitHeaderEntry {
impl GitHeaderEntry {
pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool {
let this = &self.header;
let status = status_entry.status;
let status = status_entry.file_status(repo);
match this {
Section::Conflict => {
repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path)
@@ -252,8 +252,6 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) repo_path: RepoPath,
pub(crate) status: FileStatus,
pub(crate) staging: StageStatus,
}
impl GitStatusEntry {
@@ -269,6 +267,20 @@ impl GitStatusEntry {
.parent()
.map(|parent| parent.display(path_style).to_string())
}
pub(crate) fn file_status(&self, repo: &Repository) -> FileStatus {
repo.snapshot()
.status_for_path(&self.repo_path)
.unwrap()
.status
}
fn staging(&self, panel: &GitPanel, repo: &Repository) -> StageStatus {
if let Some(staging) = panel.entry_staging(&self.repo_path) {
return staging;
}
self.file_status(repo).staging()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -279,6 +291,7 @@ enum TargetStatus {
Unchanged,
}
#[derive(Debug)]
struct PendingOperation {
finished: bool,
target_status: TargetStatus,
@@ -436,6 +449,7 @@ impl GitPanel {
| RepositoryEvent::MergeHeadsChanged,
true,
) => {
println!("Event::RepositoryUpdated with full_scan");
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryUpdated(
@@ -445,6 +459,7 @@ impl GitPanel {
)
| GitStoreEvent::RepositoryAdded
| GitStoreEvent::RepositoryRemoved(_) => {
println!("Event::RepositoryUpdated with full_scan");
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {
@@ -814,11 +829,9 @@ impl GitPanel {
) {
maybe!({
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
let active_repo = self.active_repository.as_ref()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path, cx)?;
if entry.status.is_deleted() {
let active_repo = self.active_repository.as_ref()?.read(cx);
let path = active_repo.repo_path_to_project_path(&entry.repo_path, cx)?;
if entry.file_status(active_repo).is_deleted() {
return None;
}
@@ -844,7 +857,10 @@ impl GitPanel {
maybe!({
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_entry.status_entry()?.to_owned();
let skip_prompt = action.skip_prompt || entry.status.is_created();
let skip_prompt = action.skip_prompt
|| entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created();
let prompt = if skip_prompt {
Task::ready(Ok(0))
@@ -893,7 +909,10 @@ impl GitPanel {
let list_entry = self.entries.get(self.selected_entry?)?.clone();
let entry = list_entry.status_entry()?.to_owned();
if !entry.status.is_created() {
if !entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created()
{
return Some(());
}
@@ -962,17 +981,16 @@ impl GitPanel {
) {
maybe!({
let active_repo = self.active_repository.clone()?;
let path = active_repo
.read(cx)
.repo_path_to_project_path(&entry.repo_path, cx)?;
let repo = active_repo.read(cx);
let path = repo.repo_path_to_project_path(&entry.repo_path, cx)?;
let workspace = self.workspace.clone();
if entry.status.staging().has_staged() {
if entry.staging(self, repo).has_staged() {
self.change_file_stage(false, vec![entry.clone()], cx);
}
let filename = path.path.file_name()?.to_string();
if !entry.status.is_created() {
if !entry.file_status(active_repo.read(cx)).is_created() {
self.perform_checkout(vec![entry.clone()], window, cx);
} else {
let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
@@ -1102,7 +1120,11 @@ impl GitPanel {
.entries
.iter()
.filter_map(|entry| entry.status_entry().cloned())
.filter(|status_entry| !status_entry.status.is_created())
.filter(|status_entry| {
!status_entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created()
})
.collect::<Vec<_>>();
match entries.len() {
@@ -1152,7 +1174,11 @@ impl GitPanel {
.entries
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| status_entry.status.is_created())
.filter(|status_entry| {
status_entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created()
})
.cloned()
.collect::<Vec<_>>();
@@ -1198,11 +1224,17 @@ impl GitPanel {
})
.collect::<Vec<_>>()
})?;
let to_unstage = to_delete
.into_iter()
.filter(|entry| !entry.status.staging().is_fully_unstaged())
.collect();
this.update(cx, |this, cx| this.change_file_stage(false, to_unstage, cx))?;
this.update(cx, |this, cx| {
let to_unstage = to_delete
.into_iter()
.filter(|entry| {
!entry
.staging(this, active_repo.read(cx))
.is_fully_unstaged()
})
.collect();
this.change_file_stage(false, to_unstage, cx)
})?;
for task in tasks {
task.await?;
}
@@ -1218,7 +1250,11 @@ impl GitPanel {
.entries
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| status_entry.staging.has_unstaged())
.filter(|status_entry| {
status_entry
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
.has_unstaged()
})
.cloned()
.collect::<Vec<_>>();
self.change_file_stage(true, entries, cx);
@@ -1229,7 +1265,11 @@ impl GitPanel {
.entries
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| status_entry.staging.has_staged())
.filter(|status_entry| {
status_entry
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
.has_staged()
})
.cloned()
.collect::<Vec<_>>();
self.change_file_stage(false, entries, cx);
@@ -1241,15 +1281,18 @@ impl GitPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) {
println!(" toggle_staged_for_entry");
let Some(active_repository) = self.active_repository.as_ref() else {
return;
};
let repo = active_repository.read(cx);
let (stage, repo_paths) = match entry {
GitListEntry::Status(status_entry) => {
let repo_paths = vec![status_entry.clone()];
let stage = if let Some(status) = self.entry_staging(&status_entry) {
!status.is_fully_staged()
} else if status_entry.status.staging().is_fully_staged() {
println!(" {}", status_entry.repo_path.as_unix_str());
println!(" >>> pending_ops: {:?}", self.pending);
println!(" >>> entry: {:?}", status_entry);
let stage = if status_entry.staging(self, repo).is_fully_staged() {
if let Some(op) = self.bulk_staging.clone()
&& op.anchor == status_entry.repo_path
{
@@ -1260,18 +1303,18 @@ impl GitPanel {
self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx);
true
};
println!(" >>> stage={stage}");
(stage, repo_paths)
}
GitListEntry::Header(section) => {
let goal_staged_state = !self.header_state(section.header).selected();
let repository = active_repository.read(cx);
let entries = self
.entries
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| {
section.contains(status_entry, repository)
&& status_entry.staging.as_bool() != Some(goal_staged_state)
section.contains(status_entry, repo)
&& status_entry.staging(self, repo).as_bool() != Some(goal_staged_state)
})
.cloned()
.collect::<Vec<_>>();
@@ -1455,7 +1498,9 @@ impl GitPanel {
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Staged {
if status_entry.staging(self, self.active_repository.as_ref().unwrap().read(cx))
!= StageStatus::Staged
{
self.change_file_stage(true, vec![status_entry.clone()], cx);
}
}
@@ -1472,7 +1517,9 @@ impl GitPanel {
let Some(status_entry) = selected_entry.status_entry() else {
return;
};
if status_entry.staging != StageStatus::Unstaged {
if status_entry.staging(self, self.active_repository.as_ref().unwrap().read(cx))
!= StageStatus::Unstaged
{
self.change_file_stage(false, vec![status_entry.clone()], cx);
}
}
@@ -1649,7 +1696,11 @@ impl GitPanel {
.entries
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| !status_entry.status.is_created())
.filter(|status_entry| {
!status_entry
.file_status(active_repository.read(cx))
.is_created()
})
.map(|status_entry| status_entry.repo_path.clone())
.collect::<Vec<_>>();
@@ -1794,11 +1845,20 @@ impl GitPanel {
None
}?;
let action_text = if git_status_entry.status.is_deleted() {
let action_text = if git_status_entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_deleted()
{
Some("Delete")
} else if git_status_entry.status.is_created() {
} else if git_status_entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created()
{
Some("Create")
} else if git_status_entry.status.is_modified() {
} else if git_status_entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_modified()
{
Some("Update")
} else {
None
@@ -2582,7 +2642,7 @@ impl GitPanel {
let handle = cx.entity().downgrade();
self.reopen_commit_buffer(window, cx);
self.update_visible_entries_task = cx.spawn_in(window, async move |_, cx| {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
// cx.background_executor().timer(UPDATE_DEBOUNCE).await;
if let Some(git_panel) = handle.upgrade() {
git_panel
.update_in(cx, |git_panel, window, cx| {
@@ -2638,10 +2698,12 @@ impl GitPanel {
}
fn clear_pending(&mut self) {
println!("*** CLEAR ***");
self.pending.retain(|v| !v.finished)
}
fn update_visible_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
println!("*** update_visible_entries ***");
let path_style = self.project.read(cx).path_style(cx);
let bulk_staging = self.bulk_staging.take();
let last_staged_path_prev_index = bulk_staging
@@ -2693,8 +2755,6 @@ impl GitPanel {
let entry = GitStatusEntry {
repo_path: entry.repo_path.clone(),
status: entry.status,
staging,
};
if staging.has_staged() {
@@ -2800,7 +2860,7 @@ impl GitPanel {
&& let Some(index) = bulk_staging_anchor_new_index
&& let Some(entry) = self.entries.get(index)
&& let Some(entry) = entry.status_entry()
&& self.entry_staging(entry).unwrap_or(entry.staging) == StageStatus::Staged
&& entry.staging(self, repo) == StageStatus::Staged
{
self.bulk_staging = bulk_staging;
}
@@ -2848,38 +2908,26 @@ impl GitPanel {
self.entry_count += 1;
if repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) {
self.conflicted_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if status_entry.staging(self, repo).has_staged() {
self.conflicted_staged_count += 1;
}
} else if status_entry.status.is_created() {
} else if status_entry.file_status(repo).is_created() {
self.new_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if status_entry.staging(self, repo).has_staged() {
self.new_staged_count += 1;
}
} else {
self.tracked_count += 1;
if self
.entry_staging(status_entry)
.unwrap_or(status_entry.staging)
.has_staged()
{
if status_entry.staging(self, repo).has_staged() {
self.tracked_staged_count += 1;
}
}
}
}
fn entry_staging(&self, entry: &GitStatusEntry) -> Option<StageStatus> {
fn entry_staging(&self, repo_path: &RepoPath) -> Option<StageStatus> {
for pending in self.pending.iter().rev() {
if pending.contains_path(&entry.repo_path) {
if pending.contains_path(repo_path) {
match pending.target_status {
TargetStatus::Staged => return Some(StageStatus::Staged),
TargetStatus::Unstaged => return Some(StageStatus::Unstaged),
@@ -3729,7 +3777,7 @@ impl GitPanel {
let entry = self.entries.get(ix)?;
let status = entry.status_entry()?;
let entry_staging = self.entry_staging(status).unwrap_or(status.staging);
let entry_staging = status.staging(self, repo);
let checkbox = Checkbox::new("stage-file", entry_staging.as_bool().into())
.disabled(!self.has_write_access(cx))
@@ -3767,6 +3815,7 @@ impl GitPanel {
cx: &mut Context<Self>,
) -> impl IntoElement {
let entry_count = self.entries.len();
println!("render_entries");
v_flex()
.flex_1()
@@ -3900,12 +3949,16 @@ impl GitPanel {
let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
return;
};
let stage_title = if entry.status.staging().is_fully_staged() {
let file_status = entry.file_status(self.active_repository.as_ref().unwrap().read(cx));
let stage_title = if entry
.staging(self, self.active_repository.as_ref().unwrap().read(cx))
.is_fully_staged()
{
"Unstage File"
} else {
"Stage File"
};
let restore_title = if entry.status.is_created() {
let restore_title = if file_status.is_created() {
"Trash File"
} else {
"Restore File"
@@ -3916,7 +3969,7 @@ impl GitPanel {
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile::default().boxed_clone());
if entry.status.is_created() {
if file_status.is_created() {
context_menu =
context_menu.action("Add to .gitignore", git::AddToGitignore.boxed_clone());
}
@@ -3984,13 +4037,14 @@ impl GitPanel {
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
println!(" render_entry");
let path_style = self.project.read(cx).path_style(cx);
let display_name = entry.display_name(path_style);
let selected = self.selected_entry == Some(ix);
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
let status = entry.status;
let status = entry.file_status(self.active_repository.as_ref().unwrap().read(cx));
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
@@ -4023,11 +4077,17 @@ impl GitPanel {
let checkbox_id: ElementId =
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
let entry_staging = self.entry_staging(entry).unwrap_or(entry.staging);
let entry_staging = entry.staging(self, self.active_repository.as_ref().unwrap().read(cx));
let mut is_staged: ToggleState = entry_staging.as_bool().into();
if self.show_placeholders && !self.has_staged_changes() && !entry.status.is_created() {
if self.show_placeholders
&& !self.has_staged_changes()
&& !entry
.file_status(self.active_repository.as_ref().unwrap().read(cx))
.is_created()
{
is_staged = ToggleState::Selected;
}
println!(" Checkbox status = {is_staged:?}, entry.staging = {entry_staging:?}");
let handle = cx.weak_entity();
@@ -4124,6 +4184,14 @@ impl GitPanel {
let entry = entry.clone();
let this = cx.weak_entity();
move |_, click, window, cx| {
println!(
" {} - *** Clicked - checkbox {} ***",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
entry_staging.is_fully_staged()
);
this.update(cx, |this, cx| {
if !has_write_access {
return;

View File

@@ -335,7 +335,7 @@ impl ProjectDiff {
return;
};
let repo = git_repo.read(cx);
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.file_status(repo), cx);
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0);
self.move_to_path(path_key, window, cx)

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

@@ -231,7 +231,9 @@ impl PlatformTextSystem for DirectWriteTextSystem {
Ok(*font_id)
} else {
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let font_id = lock.select_font(font);
let font_id = lock
.select_font(font)
.with_context(|| format!("Failed to select font: {:?}", font))?;
lock.font_selections.insert(font.clone(), font_id);
Ok(font_id)
}
@@ -457,7 +459,7 @@ impl DirectWriteState {
}
}
fn select_font(&mut self, target_font: &Font) -> FontId {
fn select_font(&mut self, target_font: &Font) -> Option<FontId> {
unsafe {
if target_font.family == ".SystemUIFont" {
let family = self.system_ui_font_name.clone();
@@ -468,7 +470,6 @@ impl DirectWriteState {
&target_font.features,
target_font.fallbacks.as_ref(),
)
.unwrap()
} else {
let family = self.system_ui_font_name.clone();
self.find_font_id(
@@ -478,7 +479,7 @@ impl DirectWriteState {
&target_font.features,
target_font.fallbacks.as_ref(),
)
.unwrap_or_else(|| {
.or_else(|| {
#[cfg(any(test, feature = "test-support"))]
{
panic!("ERROR: {} font not found!", target_font.family);
@@ -494,7 +495,6 @@ impl DirectWriteState {
target_font.fallbacks.as_ref(),
true,
)
.unwrap()
}
})
}
@@ -1479,8 +1479,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
.get(&font_identifier)
{
*id
} else if let Some(id) = context.text_system.select_font(&font_struct) {
id
} else {
context.text_system.select_font(&font_struct)
return Err(Error::new(DWRITE_E_NOFONT, "Failed to select font"));
};
let glyph_ids = unsafe { std::slice::from_raw_parts(glyphrun.glyphIndices, glyph_count) };

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

@@ -452,8 +452,9 @@ impl WindowsWindow {
// Failure to create a `WindowsWindowState` can cause window creation to fail,
// so check the inner result first.
let this = context.inner.take().unwrap()?;
let this = context.inner.take().transpose()?;
let hwnd = creation_result?;
let this = this.unwrap();
register_drag_drop(&this)?;
configure_dwm_dark_mode(hwnd, appearance);
@@ -900,9 +901,9 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
if idata.u.hGlobal.is_invalid() {
return Ok(());
}
let hdrop = idata.u.hGlobal.0 as *mut HDROP;
let hdrop = HDROP(idata.u.hGlobal.0);
let mut paths = SmallVec::<[PathBuf; 2]>::new();
with_file_names(*hdrop, |file_name| {
with_file_names(hdrop, |file_name| {
if let Some(path) = PathBuf::from_str(&file_name).log_err() {
paths.push(path);
}

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

@@ -1148,9 +1148,9 @@ impl MultiBuffer {
let mut counts: Vec<usize> = Vec::new();
for range in expanded_ranges {
if let Some(last_range) = merged_ranges.last_mut() {
debug_assert!(
assert!(
last_range.context.start <= range.context.start,
"Last range: {last_range:?} Range: {range:?}"
"ranges must be sorted: {last_range:?} <= {range:?}"
);
if last_range.context.end >= range.context.start
|| last_range.context.end.row + 1 == range.context.start.row

View File

@@ -172,7 +172,7 @@ impl MultiBuffer {
.into_iter()
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
.into_iter()
.flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
.filter_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
.collect::<Vec<_>>();
let snapshot = self.snapshot(cx);
@@ -280,7 +280,7 @@ impl MultiBuffer {
.excerpts_by_path
.range(..path.clone())
.next_back()
.map(|(_, value)| *value.last().unwrap())
.and_then(|(_, value)| value.last().copied())
.unwrap_or(ExcerptId::min());
let existing = self
@@ -299,6 +299,7 @@ impl MultiBuffer {
let snapshot = self.snapshot(cx);
let mut next_excerpt_id =
// is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping?
if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() {
last_entry.id.0 + 1
} else {
@@ -311,20 +312,16 @@ impl MultiBuffer {
excerpts_cursor.next();
loop {
let new = new_iter.peek();
let existing = if let Some(existing_id) = existing_iter.peek() {
let locator = snapshot.excerpt_locator_for_id(*existing_id);
let existing = if let Some(&existing_id) = existing_iter.peek() {
let locator = snapshot.excerpt_locator_for_id(existing_id);
excerpts_cursor.seek_forward(&Some(locator), Bias::Left);
if let Some(excerpt) = excerpts_cursor.item() {
if excerpt.buffer_id != buffer_snapshot.remote_id() {
to_remove.push(*existing_id);
to_remove.push(existing_id);
existing_iter.next();
continue;
}
Some((
*existing_id,
excerpt.range.context.to_point(buffer_snapshot),
))
Some((existing_id, excerpt.range.context.to_point(buffer_snapshot)))
} else {
None
}
@@ -332,6 +329,7 @@ impl MultiBuffer {
None
};
let new = new_iter.peek();
if let Some((last_id, last)) = to_insert.last_mut() {
if let Some(new) = new
&& last.context.end >= new.context.start

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

@@ -4815,10 +4815,11 @@ impl Repository {
}),
)
.collect::<Vec<_>>();
if !edits.is_empty() {
let full_scan = !edits.is_empty();
self.snapshot.statuses_by_path.edit(edits, ());
if full_scan {
cx.emit(RepositoryEvent::StatusesChanged { full_scan: true });
}
self.snapshot.statuses_by_path.edit(edits, ());
if update.is_last_update {
self.snapshot.scan_id = update.scan_id;
}

View File

@@ -4193,7 +4193,7 @@ impl LspStore {
})
.detach();
} else {
panic!("oops!");
// Our remote connection got closed
}
handle
}
@@ -7643,7 +7643,10 @@ impl LspStore {
let buffer = buffer.read(cx);
let file = File::from_dyn(buffer.file())?;
let abs_path = file.as_local()?.abs_path(cx);
let uri = lsp::Uri::from_file_path(abs_path).unwrap();
let uri = lsp::Uri::from_file_path(&abs_path)
.ok()
.with_context(|| format!("Failed to convert path to URI: {}", abs_path.display()))
.unwrap();
let next_snapshot = buffer.text_snapshot();
for language_server in language_servers {
let language_server = language_server.clone();

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?;
@@ -850,7 +850,7 @@ impl SshRemoteConnection {
if let Some(mut stdin) = child.stdin.take() {
use futures::AsyncWriteExt;
stdin.write_all(sftp_batch.as_bytes()).await?;
drop(stdin);
stdin.flush().await?;
}
let output = child.output().await?;

View File

@@ -19,6 +19,7 @@ use std::{
time::Instant,
};
use util::{
ResultExt as _,
paths::{PathStyle, RemotePathBuf},
rel_path::RelPath,
shell::ShellKind,
@@ -79,43 +80,42 @@ impl WslRemoteConnection {
can_exec: true,
};
delegate.set_status(Some("Detecting WSL environment"), cx);
this.shell = this.detect_shell().await?;
this.shell = this
.detect_shell()
.await
.context("failed detecting shell")?;
this.shell_kind = ShellKind::new(&this.shell, false);
this.can_exec = this.detect_can_exec().await?;
this.platform = this.detect_platform().await?;
this.can_exec = this.detect_can_exec().await;
this.platform = this
.detect_platform()
.await
.context("failed detecting platform")?;
this.remote_binary_path = Some(
this.ensure_server_binary(&delegate, release_channel, version, commit, cx)
.await?,
.await
.context("failed ensuring server binary")?,
);
log::debug!("Detected WSL environment: {this:#?}");
Ok(this)
}
async fn detect_can_exec(&self) -> Result<bool> {
async fn detect_can_exec(&self) -> bool {
let options = &self.connection_options;
let program = self.shell_kind.prepend_command_prefix("uname");
let args = &["-m"];
let output = wsl_command_impl(options, &program, args, true)
.output()
.await?;
.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()
));
}
Ok(false)
if !output.is_ok_and(|output| output.status.success()) {
run_wsl_command_impl(options, &program, args, false)
.await
.context("failed detecting exec status")
.log_err();
false
} else {
Ok(true)
true
}
}
async fn detect_platform(&self) -> Result<RemotePlatform> {
@@ -517,7 +517,9 @@ impl RemoteConnection for WslRemoteConnection {
/// `wslpath` is a executable available in WSL, it's a linux binary.
/// So it doesn't support Windows style paths.
async fn sanitize_path(path: &Path) -> Result<String> {
let path = smol::fs::canonicalize(path).await?;
let path = smol::fs::canonicalize(path)
.await
.with_context(|| format!("Failed to canonicalize path {}", path.display()))?;
let path_str = path.to_string_lossy();
let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
@@ -539,14 +541,16 @@ async fn run_wsl_command_impl(
args: &[&str],
exec: bool,
) -> Result<String> {
let output = wsl_command_impl(options, program, args, exec)
let mut command = wsl_command_impl(options, program, args, exec);
let output = command
.output()
.await?;
.await
.with_context(|| format!("Failed to run command '{:?}'", command))?;
if !output.status.success() {
return Err(anyhow!(
"Command '{}' failed: {}",
program,
"Command '{:?}' failed: {}",
command,
String::from_utf8_lossy(&output.stderr).trim()
));
}

View File

@@ -448,7 +448,7 @@ where
aggregate: &mut dyn SeekAggregate<'a, T>,
) -> bool {
assert!(
target.cmp(&self.position, self.cx) >= Ordering::Equal,
target.cmp(&self.position, self.cx).is_ge(),
"cannot seek backward",
);

View File

@@ -387,7 +387,7 @@ impl TerminalBuilder {
selection_phase: SelectionPhase::Ended,
hyperlink_regex_searches: RegexSearches::new(),
vi_mode_enabled: false,
is_ssh_terminal: false,
is_remote_terminal: false,
last_mouse_move_time: Instant::now(),
last_hyperlink_search_position: None,
#[cfg(windows)]
@@ -419,14 +419,14 @@ impl TerminalBuilder {
cursor_shape: CursorShape,
alternate_scroll: AlternateScroll,
max_scroll_history_lines: Option<usize>,
is_ssh_terminal: bool,
is_remote_terminal: bool,
window_id: u64,
completion_tx: Option<Sender<Option<ExitStatus>>>,
cx: &App,
activation_script: Vec<String>,
) -> Task<Result<TerminalBuilder>> {
let version = release_channel::AppVersion::global(cx);
cx.background_spawn(async move {
let fut = async move {
// If the parent environment doesn't have a locale set
// (As is the case when launched from a .app on MacOS),
// and the Project doesn't have a locale set, then
@@ -605,7 +605,7 @@ impl TerminalBuilder {
selection_phase: SelectionPhase::Ended,
hyperlink_regex_searches: RegexSearches::new(),
vi_mode_enabled: false,
is_ssh_terminal,
is_remote_terminal,
last_mouse_move_time: Instant::now(),
last_hyperlink_search_position: None,
#[cfg(windows)]
@@ -648,7 +648,13 @@ impl TerminalBuilder {
terminal,
events_rx,
})
})
};
// the thread we spawn things on has an effect on signal handling
if !cfg!(target_os = "windows") {
cx.spawn(async move |_| fut.await)
} else {
cx.background_spawn(fut)
}
}
pub fn subscribe(mut self, cx: &Context<Terminal>) -> Terminal {
@@ -826,7 +832,7 @@ pub struct Terminal {
hyperlink_regex_searches: RegexSearches,
task: Option<TaskState>,
vi_mode_enabled: bool,
is_ssh_terminal: bool,
is_remote_terminal: bool,
last_mouse_move_time: Instant,
last_hyperlink_search_position: Option<Point<Pixels>>,
#[cfg(windows)]
@@ -1957,7 +1963,7 @@ impl Terminal {
}
pub fn working_directory(&self) -> Option<PathBuf> {
if self.is_ssh_terminal {
if self.is_remote_terminal {
// We can't yet reliably detect the working directory of a shell on the
// SSH host. Until we can do that, it doesn't make sense to display
// the working directory on the client and persist that.
@@ -2156,7 +2162,7 @@ impl Terminal {
self.template.cursor_shape,
self.template.alternate_scroll,
self.template.max_scroll_history_lines,
self.is_ssh_terminal,
self.is_remote_terminal,
self.template.window_id,
None,
cx,

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.3"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
stable

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,136 @@
use gh_workflow::*;
use crate::tasks::workflows::{
release, runners,
steps::{NamedJob, checkout_repo, 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(&[]);
let publish_winget = publish_winget();
let create_sentry_release = create_sentry_release();
named::workflow()
.on(Event::default().release(Release::default().types(vec![ReleaseType::Published])))
.add_job(post_to_discord.name, post_to_discord.job)
.add_job(publish_winget.name, publish_winget.job)
.add_job(create_sentry_release.name, create_sentry_release.job)
}
#[allow(unused)]
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)),
)
}
fn create_sentry_release() -> NamedJob {
let job = Job::default()
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(
"github.repository_owner == 'zed-industries'",
))
.add_step(checkout_repo())
.add_step(release::create_sentry_release());
named::job(job)
}

View File

@@ -107,7 +107,6 @@ fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
)
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
)
.add_step(create_sentry_release()),
)
}

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