Compare commits
51 Commits
v0.212.7
...
zeta2_edit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66f4d3654a | ||
|
|
e7fd603b68 | ||
|
|
9c8e37a156 | ||
|
|
d54c64f35a | ||
|
|
0b53da18d5 | ||
|
|
5ced3ef0fd | ||
|
|
7a37dd9433 | ||
|
|
a7163623e7 | ||
|
|
f08068680d | ||
|
|
a951e414d8 | ||
|
|
e75c6b1aa5 | ||
|
|
149eedb73d | ||
|
|
fb46bae3ed | ||
|
|
28d7c37b0d | ||
|
|
f6da987d4c | ||
|
|
efc71f35a5 | ||
|
|
3b7ee58cfa | ||
|
|
32047bef93 | ||
|
|
4003287cc3 | ||
|
|
001a47c8b7 | ||
|
|
113f0780b3 | ||
|
|
273321608f | ||
|
|
92cfce568b | ||
|
|
2b6cf31ace | ||
|
|
58cec41932 | ||
|
|
2ec5ca0e05 | ||
|
|
f8da550867 | ||
|
|
0b1d3d78a4 | ||
|
|
930b489d90 | ||
|
|
121cee8045 | ||
|
|
5360dc1504 | ||
|
|
69862790cb | ||
|
|
284d8f790a | ||
|
|
f824e93eeb | ||
|
|
e71bc4821c | ||
|
|
64c8c19e1b | ||
|
|
622d626a29 | ||
|
|
714481073d | ||
|
|
eccdfed32b | ||
|
|
2664596a34 | ||
|
|
23f2fb6089 | ||
|
|
fb2c2c55dc | ||
|
|
8315fde1ff | ||
|
|
fc87440682 | ||
|
|
c996eadaf5 | ||
|
|
e8c6c1ba04 | ||
|
|
b8364d7c33 | ||
|
|
7c23ef89ec | ||
|
|
2f463370cc | ||
|
|
feed34cafe | ||
|
|
4724aa5cb8 |
69
.github/workflows/after_release.yml
vendored
Normal file
69
.github/workflows/after_release.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
# Generated from xtask::workflows::after_release
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: after_release
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
jobs:
|
||||
rebuild_releases_page:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: after_release::rebuild_releases_page
|
||||
run: 'curl https://zed.dev/api/revalidate-releases -H "Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}"'
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
RELEASE_NOTES_API_TOKEN: ${{ secrets.RELEASE_NOTES_API_TOKEN }}
|
||||
post_to_discord:
|
||||
needs:
|
||||
- rebuild_releases_page
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-release-url
|
||||
name: after_release::post_to_discord::get_release_url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: get-content
|
||||
name: after_release::post_to_discord::get_content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
truncationSymbol: '...'
|
||||
- name: after_release::post_to_discord::discord_webhook_action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
publish_winget:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: set-package-name
|
||||
name: after_release::publish_winget::set_package_name
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: after_release::publish_winget::winget_releaser
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
93
.github/workflows/community_release_actions.yml
vendored
93
.github/workflows/community_release_actions.yml
vendored
@@ -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
|
||||
70
.github/workflows/run_tests.yml
vendored
70
.github/workflows/run_tests.yml
vendored
@@ -285,40 +285,6 @@ jobs:
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
timeout-minutes: 60
|
||||
check_postgres_and_protobuf_migrations:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.run_tests == 'true'
|
||||
runs-on: self-mini-macos
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
|
||||
run: git clean -df
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ];
|
||||
then
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
|
||||
else
|
||||
git checkout -B temp
|
||||
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
|
||||
uses: bufbuild/buf-setup-action@v1
|
||||
with:
|
||||
version: v1.29.0
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
|
||||
uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: crates/proto/proto/
|
||||
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
|
||||
timeout-minutes: 60
|
||||
check_dependencies:
|
||||
needs:
|
||||
- orchestrate
|
||||
@@ -518,6 +484,40 @@ jobs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
check_postgres_and_protobuf_migrations:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.run_tests == 'true'
|
||||
runs-on: self-mini-macos
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
|
||||
run: git clean -df
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ];
|
||||
then
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
|
||||
else
|
||||
git checkout -B temp
|
||||
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
|
||||
uses: bufbuild/buf-setup-action@v1
|
||||
with:
|
||||
version: v1.29.0
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
|
||||
uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: crates/proto/proto/
|
||||
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
|
||||
timeout-minutes: 60
|
||||
tests_pass:
|
||||
needs:
|
||||
- orchestrate
|
||||
@@ -527,7 +527,6 @@ jobs:
|
||||
- run_tests_mac
|
||||
- doctests
|
||||
- check_workspace_binaries
|
||||
- check_postgres_and_protobuf_migrations
|
||||
- check_dependencies
|
||||
- check_docs
|
||||
- check_licenses
|
||||
@@ -554,7 +553,6 @@ jobs:
|
||||
check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}"
|
||||
check_result "doctests" "${{ needs.doctests.result }}"
|
||||
check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}"
|
||||
check_result "check_postgres_and_protobuf_migrations" "${{ needs.check_postgres_and_protobuf_migrations.result }}"
|
||||
check_result "check_dependencies" "${{ needs.check_dependencies.result }}"
|
||||
check_result "check_docs" "${{ needs.check_docs.result }}"
|
||||
check_result "check_licenses" "${{ needs.check_licenses.result }}"
|
||||
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -7091,7 +7091,6 @@ dependencies = [
|
||||
"askpass",
|
||||
"buffer_diff",
|
||||
"call",
|
||||
"chrono",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
@@ -18618,6 +18617,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
"nix 0.29.0",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.2",
|
||||
@@ -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.213.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -663,6 +663,7 @@ time = { version = "0.3", features = [
|
||||
"serde",
|
||||
"serde-well-known",
|
||||
"formatting",
|
||||
"local-offset",
|
||||
] }
|
||||
tiny_http = "0.8"
|
||||
tokio = { version = "1" }
|
||||
|
||||
@@ -717,7 +717,10 @@ impl MessageEditor {
|
||||
let mut all_tracked_buffers = Vec::new();
|
||||
|
||||
let result = editor.update(cx, |editor, cx| {
|
||||
let mut ix = text.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
|
||||
let (mut ix, _) = text
|
||||
.char_indices()
|
||||
.find(|(_, c)| !c.is_whitespace())
|
||||
.unwrap_or((0, '\0'));
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let text = editor.text(cx);
|
||||
editor.display_map.update(cx, |map, cx| {
|
||||
@@ -2879,7 +2882,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text(" hello world ", window, cx);
|
||||
editor.set_text(" \u{A0}してhello world ", window, cx);
|
||||
});
|
||||
|
||||
let (content, _) = message_editor
|
||||
@@ -2890,7 +2893,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
content,
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "hello world".into(),
|
||||
text: "してhello world".into(),
|
||||
annotations: None,
|
||||
meta: None
|
||||
})]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -2131,12 +2131,20 @@ impl AgentPanel {
|
||||
menu
|
||||
})
|
||||
.separator()
|
||||
.link(
|
||||
"Add Other Agents",
|
||||
OpenBrowser {
|
||||
url: zed_urls::external_agents_docs(cx),
|
||||
}
|
||||
.boxed_clone(),
|
||||
.item(
|
||||
ContextMenuEntry::new("Add More Agents")
|
||||
.icon(IconName::Plus)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
move |window, cx| {
|
||||
window.dispatch_action(Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::AgentServers,
|
||||
),
|
||||
id: None,
|
||||
}), cx)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}
|
||||
@@ -2150,7 +2158,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)
|
||||
})
|
||||
|
||||
@@ -406,6 +406,7 @@ impl AutoUpdater {
|
||||
arch: &str,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<PathBuf> {
|
||||
let this = cx.update(|cx| {
|
||||
@@ -415,6 +416,7 @@ impl AutoUpdater {
|
||||
.context("auto-update not initialized")
|
||||
})??;
|
||||
|
||||
set_status("Fetching remote server release", cx);
|
||||
let release = Self::get_release(
|
||||
&this,
|
||||
"zed-remote-server",
|
||||
@@ -439,6 +441,7 @@ impl AutoUpdater {
|
||||
"downloading zed-remote-server {os} {arch} version {}",
|
||||
release.version
|
||||
);
|
||||
set_status("Downloading remote server", cx);
|
||||
download_remote_server_binary(&version_path, release, client, cx).await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ impl fmt::Debug for Lamport {
|
||||
impl fmt::Debug for Global {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Global {{")?;
|
||||
for timestamp in self.iter() {
|
||||
for timestamp in self.iter().filter(|t| t.value > 0) {
|
||||
if timestamp.replica_id.0 > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
|
||||
@@ -265,9 +265,10 @@ impl minidumper::ServerHandler for CrashServer {
|
||||
3 => {
|
||||
let gpu_specs: system_specs::GpuSpecs =
|
||||
bincode::deserialize(&buffer).expect("gpu specs");
|
||||
self.active_gpu
|
||||
.set(gpu_specs)
|
||||
.expect("already set active gpu");
|
||||
// we ignore the case where it was already set because this message is sent
|
||||
// on each new window. in theory all zed windows should be using the same
|
||||
// GPU so this is fine.
|
||||
self.active_gpu.set(gpu_specs).ok();
|
||||
}
|
||||
_ => {
|
||||
panic!("invalid message kind");
|
||||
|
||||
@@ -88,10 +88,14 @@ pub fn switch_source_header(
|
||||
)
|
||||
})?;
|
||||
|
||||
let path = PathBuf::from(goto);
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let goto = if workspace.path_style(cx).is_windows() {
|
||||
goto.strip_prefix('/').unwrap_or(goto)
|
||||
} else {
|
||||
goto
|
||||
};
|
||||
let path = PathBuf::from(goto);
|
||||
workspace.open_abs_path(
|
||||
path,
|
||||
OpenOptions {
|
||||
|
||||
@@ -812,6 +812,22 @@ impl MinimapVisibility {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BufferSerialization {
|
||||
All,
|
||||
NonDirtyBuffers,
|
||||
}
|
||||
|
||||
impl BufferSerialization {
|
||||
fn new(restore_unsaved_buffers: bool) -> Self {
|
||||
if restore_unsaved_buffers {
|
||||
Self::All
|
||||
} else {
|
||||
Self::NonDirtyBuffers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RunnableTasks {
|
||||
templates: Vec<(TaskSourceKind, TaskTemplate)>,
|
||||
@@ -1129,7 +1145,7 @@ pub struct Editor {
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
git_blame_inline_enabled: bool,
|
||||
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
||||
serialize_dirty_buffers: bool,
|
||||
buffer_serialization: Option<BufferSerialization>,
|
||||
show_selection_menu: Option<bool>,
|
||||
blame: Option<Entity<GitBlame>>,
|
||||
blame_subscription: Option<Subscription>,
|
||||
@@ -1905,7 +1921,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
project::Event::EntryRenamed(transaction) => {
|
||||
project::Event::EntryRenamed(transaction, project_path, abs_path) => {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
@@ -1913,7 +1929,23 @@ impl Editor {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if active_editor.entity_id() == cx.entity_id() {
|
||||
let entity_id = cx.entity_id();
|
||||
workspace.update(cx, |this, cx| {
|
||||
this.panes_mut()
|
||||
.iter_mut()
|
||||
.filter(|pane| pane.entity_id() != entity_id)
|
||||
.for_each(|p| {
|
||||
p.update(cx, |pane, _| {
|
||||
pane.nav_history_mut().rename_item(
|
||||
entity_id,
|
||||
project_path.clone(),
|
||||
abs_path.clone().into(),
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
@@ -1936,7 +1968,6 @@ impl Editor {
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
let transaction = transaction.clone();
|
||||
@@ -2190,10 +2221,13 @@ impl Editor {
|
||||
git_blame_inline_enabled: full_mode
|
||||
&& ProjectSettings::get_global(cx).git.inline_blame.enabled,
|
||||
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
|
||||
serialize_dirty_buffers: !is_minimap
|
||||
&& ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
buffer_serialization: is_minimap.not().then(|| {
|
||||
BufferSerialization::new(
|
||||
ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
)
|
||||
}),
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
tasks: BTreeMap::default(),
|
||||
@@ -2281,6 +2315,8 @@ impl Editor {
|
||||
|editor, _, e: &EditorEvent, window, cx| match e {
|
||||
EditorEvent::ScrollPositionChanged { local, .. } => {
|
||||
if *local {
|
||||
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||
editor.inline_blame_popover.take();
|
||||
let new_anchor = editor.scroll_manager.anchor();
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.update_restoration_data(cx, move |data| {
|
||||
@@ -2289,8 +2325,22 @@ impl Editor {
|
||||
new_anchor.offset,
|
||||
);
|
||||
});
|
||||
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||
editor.inline_blame_popover.take();
|
||||
|
||||
editor.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(50))
|
||||
.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.register_visible_buffers(cx);
|
||||
editor.refresh_colors_for_visible_range(None, window, cx);
|
||||
editor.refresh_inlay_hints(
|
||||
InlayHintRefreshReason::NewLinesShown,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
@@ -2721,6 +2771,14 @@ impl Editor {
|
||||
self.workspace.as_ref()?.0.upgrade()
|
||||
}
|
||||
|
||||
/// Returns the workspace serialization ID if this editor should be serialized.
|
||||
fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> {
|
||||
self.workspace
|
||||
.as_ref()
|
||||
.filter(|_| self.should_serialize_buffer())
|
||||
.and_then(|workspace| workspace.1)
|
||||
}
|
||||
|
||||
pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> {
|
||||
self.buffer().read(cx).title(cx)
|
||||
}
|
||||
@@ -2969,6 +3027,20 @@ impl Editor {
|
||||
self.auto_replace_emoji_shortcode = auto_replace;
|
||||
}
|
||||
|
||||
pub fn set_should_serialize(&mut self, should_serialize: bool, cx: &App) {
|
||||
self.buffer_serialization = should_serialize.then(|| {
|
||||
BufferSerialization::new(
|
||||
ProjectSettings::get_global(cx)
|
||||
.session
|
||||
.restore_unsaved_buffers,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn should_serialize_buffer(&self) -> bool {
|
||||
self.buffer_serialization.is_some()
|
||||
}
|
||||
|
||||
pub fn toggle_edit_predictions(
|
||||
&mut self,
|
||||
_: &ToggleEditPrediction,
|
||||
@@ -3185,8 +3257,7 @@ impl Editor {
|
||||
});
|
||||
|
||||
if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
|
||||
&& let Some(workspace_id) =
|
||||
self.workspace.as_ref().and_then(|workspace| workspace.1)
|
||||
&& let Some(workspace_id) = self.workspace_serialization_id(cx)
|
||||
{
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let selections = selections.clone();
|
||||
@@ -3251,7 +3322,7 @@ impl Editor {
|
||||
data.folds = inmemory_folds;
|
||||
});
|
||||
|
||||
let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else {
|
||||
let Some(workspace_id) = self.workspace_serialization_id(cx) else {
|
||||
return;
|
||||
};
|
||||
let background_executor = cx.background_executor().clone();
|
||||
@@ -21148,8 +21219,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
let project_settings = ProjectSettings::get_global(cx);
|
||||
self.serialize_dirty_buffers =
|
||||
!self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers;
|
||||
self.buffer_serialization = self
|
||||
.should_serialize_buffer()
|
||||
.then(|| BufferSerialization::new(project_settings.session.restore_unsaved_buffers));
|
||||
|
||||
if self.mode.is_full() {
|
||||
let show_inline_diagnostics = project_settings.diagnostics.inline.enabled;
|
||||
|
||||
@@ -1701,9 +1701,7 @@ impl EditorElement {
|
||||
len,
|
||||
font,
|
||||
color,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None,
|
||||
)
|
||||
@@ -3583,9 +3581,7 @@ impl EditorElement {
|
||||
len: line.len(),
|
||||
font: style.text.font(),
|
||||
color: placeholder_color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
let line = window.text_system().shape_line(
|
||||
line.to_string().into(),
|
||||
@@ -3907,11 +3903,11 @@ impl EditorElement {
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.rounded_sm()
|
||||
.gap_1p5()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
@@ -3932,15 +3928,17 @@ impl EditorElement {
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
let button_size = rems_from_px(28.);
|
||||
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.style(ButtonStyle::Transparent)
|
||||
.height(button_size.into())
|
||||
.width(button_size)
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -3996,7 +3994,13 @@ impl EditorElement {
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(h_flex().size(px(12.0)).justify_center().children(indicator))
|
||||
.child(
|
||||
h_flex()
|
||||
.size(rems_from_px(12.0))
|
||||
.justify_center()
|
||||
.flex_shrink_0()
|
||||
.children(indicator),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
@@ -4004,41 +4008,51 @@ impl EditorElement {
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
.child(h_flex().gap_0p5().map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon = FileIcons::get_icon(path, cx)
|
||||
.unwrap_or_default();
|
||||
let icon =
|
||||
Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.child(Label::new(filename).single_line().when_some(
|
||||
file_status,
|
||||
|el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
},
|
||||
))
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon =
|
||||
FileIcons::get_icon(path, cx).unwrap_or_default();
|
||||
let icon = Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.child(
|
||||
ButtonLike::new("filename-button")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
Label::new(filename)
|
||||
.single_line()
|
||||
.color(file_status_label_color(
|
||||
file_status,
|
||||
))
|
||||
.when(
|
||||
file_status.is_some_and(|s| {
|
||||
s.is_deleted()
|
||||
}),
|
||||
|label| label.strikethrough(),
|
||||
),
|
||||
)
|
||||
.group_hover("", |div| div.underline()),
|
||||
)
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
let jump_data = jump_data.clone();
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
@@ -4047,33 +4061,42 @@ impl EditorElement {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
})
|
||||
}))
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
Button::new("open-file", "Open File")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
let jump_data = jump_data.clone();
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
move |editor, _e: &ClickEvent, _window, cx| {
|
||||
if is_folded {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
} else {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
}
|
||||
}
|
||||
})),
|
||||
),
|
||||
@@ -4081,6 +4104,7 @@ impl EditorElement {
|
||||
|
||||
let file = for_excerpt.buffer.file().cloned();
|
||||
let editor = self.editor.clone();
|
||||
|
||||
right_click_menu("buffer-header-context-menu")
|
||||
.trigger(move |_, _, _| header)
|
||||
.menu(move |window, cx| {
|
||||
@@ -7416,9 +7440,7 @@ impl EditorElement {
|
||||
len: column,
|
||||
font: style.text.font(),
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None,
|
||||
);
|
||||
@@ -7441,9 +7463,7 @@ impl EditorElement {
|
||||
len: text.len(),
|
||||
font: self.style.text.font(),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
window.text_system().shape_line(
|
||||
text,
|
||||
@@ -7512,6 +7532,22 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
|
||||
file_status.map_or(Color::Default, |status| {
|
||||
if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else if status.is_created() {
|
||||
Color::Created
|
||||
} else {
|
||||
Color::Default
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
@@ -9528,9 +9564,7 @@ impl Element for EditorElement {
|
||||
len: tab_len,
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None,
|
||||
);
|
||||
@@ -9544,9 +9578,7 @@ impl Element for EditorElement {
|
||||
len: space_len,
|
||||
font: self.style.text.font(),
|
||||
color: cx.theme().colors().editor_invisible,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None,
|
||||
);
|
||||
@@ -11533,11 +11565,8 @@ mod tests {
|
||||
fn generate_test_run(len: usize, color: Hsla) -> TextRun {
|
||||
TextRun {
|
||||
len,
|
||||
font: gpui::font(".SystemUIFont"),
|
||||
color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
|
||||
MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange,
|
||||
SelectionEffects, ToPoint as _,
|
||||
Anchor, Autoscroll, BufferSerialization, Editor, EditorEvent, EditorSettings, ExcerptId,
|
||||
ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, NavigationData,
|
||||
ReportEditorEvent, SearchWithinRange, SelectionEffects, ToPoint as _,
|
||||
display_map::HighlightKey,
|
||||
editor_settings::SeedQuerySetting,
|
||||
persistence::{DB, SerializedEditor},
|
||||
@@ -1256,17 +1256,15 @@ impl SerializableItem for Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if self.mode.is_minimap() {
|
||||
return None;
|
||||
}
|
||||
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
|
||||
|
||||
let buffer_serialization = self.buffer_serialization?;
|
||||
let project = self.project.clone()?;
|
||||
if project.read(cx).visible_worktrees(cx).next().is_none() {
|
||||
|
||||
let serialize_dirty_buffers = match buffer_serialization {
|
||||
// If we don't have a worktree, we don't serialize, because
|
||||
// projects without worktrees aren't deserialized.
|
||||
serialize_dirty_buffers = false;
|
||||
}
|
||||
BufferSerialization::All => project.read(cx).visible_worktrees(cx).next().is_some(),
|
||||
BufferSerialization::NonDirtyBuffers => false,
|
||||
};
|
||||
|
||||
if closing && !serialize_dirty_buffers {
|
||||
return None;
|
||||
@@ -1323,10 +1321,11 @@ impl SerializableItem for Editor {
|
||||
}
|
||||
|
||||
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
|
||||
)
|
||||
self.should_serialize_buffer()
|
||||
&& matches!(
|
||||
event,
|
||||
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -603,7 +603,7 @@ impl Editor {
|
||||
scroll_position
|
||||
};
|
||||
|
||||
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
|
||||
self.scroll_manager.set_scroll_position(
|
||||
adjusted_position,
|
||||
&display_map,
|
||||
local,
|
||||
@@ -611,22 +611,7 @@ impl Editor {
|
||||
workspace_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(50))
|
||||
.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.register_visible_buffers(cx);
|
||||
editor.refresh_colors_for_visible_range(None, window, cx);
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
editor_was_scrolled
|
||||
)
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ impl GitRepository for FakeGitRepository {
|
||||
|
||||
fn pull(
|
||||
&self,
|
||||
_branch: String,
|
||||
_branch: Option<String>,
|
||||
_remote: String,
|
||||
_rebase: bool,
|
||||
_askpass: AskPassDelegate,
|
||||
|
||||
@@ -531,7 +531,7 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
fn pull(
|
||||
&self,
|
||||
branch_name: String,
|
||||
branch_name: Option<String>,
|
||||
upstream_name: String,
|
||||
rebase: bool,
|
||||
askpass: AskPassDelegate,
|
||||
@@ -1689,7 +1689,7 @@ impl GitRepository for RealGitRepository {
|
||||
|
||||
fn pull(
|
||||
&self,
|
||||
branch_name: String,
|
||||
branch_name: Option<String>,
|
||||
remote_name: String,
|
||||
rebase: bool,
|
||||
ask_pass: AskPassDelegate,
|
||||
@@ -1713,7 +1713,7 @@ impl GitRepository for RealGitRepository {
|
||||
|
||||
command
|
||||
.arg(remote_name)
|
||||
.arg(branch_name)
|
||||
.args(branch_name)
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
.stderr(smol::process::Stdio::piped());
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ pub fn init(cx: &mut App) {
|
||||
let provider_registry = GitHostingProviderRegistry::global(cx);
|
||||
provider_registry.register_hosting_provider(Arc::new(Bitbucket::public_instance()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Chromium));
|
||||
provider_registry.register_hosting_provider(Arc::new(Codeberg));
|
||||
provider_registry.register_hosting_provider(Arc::new(Forgejo::public_instance()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Gitea::public_instance()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Gitee));
|
||||
provider_registry.register_hosting_provider(Arc::new(Github::public_instance()));
|
||||
provider_registry.register_hosting_provider(Arc::new(Gitlab::public_instance()));
|
||||
@@ -44,6 +45,10 @@ pub fn register_additional_providers(
|
||||
provider_registry.register_hosting_provider(Arc::new(gitlab_self_hosted));
|
||||
} else if let Ok(github_self_hosted) = Github::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(github_self_hosted));
|
||||
} else if let Ok(forgejo_self_hosted) = Forgejo::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(forgejo_self_hosted));
|
||||
} else if let Ok(gitea_self_hosted) = Gitea::from_remote_url(&origin_url) {
|
||||
provider_registry.register_hosting_provider(Arc::new(gitea_self_hosted));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod bitbucket;
|
||||
mod chromium;
|
||||
mod codeberg;
|
||||
mod forgejo;
|
||||
mod gitea;
|
||||
mod gitee;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
@@ -8,7 +9,8 @@ mod sourcehut;
|
||||
|
||||
pub use bitbucket::*;
|
||||
pub use chromium::*;
|
||||
pub use codeberg::*;
|
||||
pub use forgejo::*;
|
||||
pub use gitea::*;
|
||||
pub use gitee::*;
|
||||
pub use github::*;
|
||||
pub use gitlab::*;
|
||||
|
||||
@@ -14,6 +14,8 @@ use git::{
|
||||
RemoteUrl,
|
||||
};
|
||||
|
||||
use crate::get_host_from_git_remote_url;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CommitDetails {
|
||||
#[expect(
|
||||
@@ -67,31 +69,72 @@ struct User {
|
||||
pub avatar_url: String,
|
||||
}
|
||||
|
||||
pub struct Codeberg;
|
||||
pub struct Forgejo {
|
||||
name: String,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
impl Codeberg {
|
||||
async fn fetch_codeberg_commit_author(
|
||||
impl Forgejo {
|
||||
pub fn new(name: impl Into<String>, base_url: Url) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_instance() -> Self {
|
||||
Self::new("Codeberg", Url::parse("https://codeberg.org").unwrap())
|
||||
}
|
||||
|
||||
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
|
||||
let host = get_host_from_git_remote_url(remote_url)?;
|
||||
if host == "codeberg.org" {
|
||||
bail!("the Forgejo instance is not self-hosted");
|
||||
}
|
||||
|
||||
// TODO: detecting self hosted instances by checking whether "forgejo" is in the url or not
|
||||
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
|
||||
// information.
|
||||
if !host.contains("forgejo") {
|
||||
bail!("not a Forgejo URL");
|
||||
}
|
||||
|
||||
Ok(Self::new(
|
||||
"Forgejo Self-Hosted",
|
||||
Url::parse(&format!("https://{}", host))?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn fetch_forgejo_commit_author(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
repo: &str,
|
||||
commit: &str,
|
||||
client: &Arc<dyn HttpClient>,
|
||||
) -> Result<Option<User>> {
|
||||
let url =
|
||||
format!("https://codeberg.org/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}");
|
||||
let Some(host) = self.base_url.host_str() else {
|
||||
bail!("failed to get host from forgejo base url");
|
||||
};
|
||||
let url = format!(
|
||||
"https://{host}/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}?stat=false&verification=false&files=false"
|
||||
);
|
||||
|
||||
let mut request = Request::get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.follow_redirects(http_client::RedirectPolicy::FollowAll);
|
||||
|
||||
if let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN") {
|
||||
// TODO: not renamed yet for compatibility reasons, may require a refactor later
|
||||
// see https://github.com/zed-industries/zed/issues/11043#issuecomment-3480446231
|
||||
if host == "codeberg.org"
|
||||
&& let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN")
|
||||
{
|
||||
request = request.header("Authorization", format!("Bearer {}", codeberg_token));
|
||||
}
|
||||
|
||||
let mut response = client
|
||||
.send(request.body(AsyncBody::default())?)
|
||||
.await
|
||||
.with_context(|| format!("error fetching Codeberg commit details at {:?}", url))?;
|
||||
.with_context(|| format!("error fetching Forgejo commit details at {:?}", url))?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
@@ -108,18 +151,18 @@ impl Codeberg {
|
||||
|
||||
serde_json::from_str::<CommitDetails>(body_str)
|
||||
.map(|commit| commit.author)
|
||||
.context("failed to deserialize Codeberg commit details")
|
||||
.context("failed to deserialize Forgejo commit details")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GitHostingProvider for Codeberg {
|
||||
impl GitHostingProvider for Forgejo {
|
||||
fn name(&self) -> String {
|
||||
"Codeberg".to_string()
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn base_url(&self) -> Url {
|
||||
Url::parse("https://codeberg.org").unwrap()
|
||||
self.base_url.clone()
|
||||
}
|
||||
|
||||
fn supports_avatars(&self) -> bool {
|
||||
@@ -138,7 +181,7 @@ impl GitHostingProvider for Codeberg {
|
||||
let url = RemoteUrl::from_str(url).ok()?;
|
||||
|
||||
let host = url.host_str()?;
|
||||
if host != "codeberg.org" {
|
||||
if host != self.base_url.host_str()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -194,9 +237,27 @@ impl GitHostingProvider for Codeberg {
|
||||
) -> Result<Option<Url>> {
|
||||
let commit = commit.to_string();
|
||||
let avatar_url = self
|
||||
.fetch_codeberg_commit_author(repo_owner, repo, &commit, &http_client)
|
||||
.fetch_forgejo_commit_author(repo_owner, repo, &commit, &http_client)
|
||||
.await?
|
||||
.map(|author| Url::parse(&author.avatar_url))
|
||||
.map(|author| -> Result<Url, url::ParseError> {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
if let Some(host) = url.host_str() {
|
||||
let size_query = if host.contains("gravatar") || host.contains("libravatar") {
|
||||
Some("s=128")
|
||||
} else if self
|
||||
.base_url
|
||||
.host_str()
|
||||
.is_some_and(|base_host| host.contains(base_host))
|
||||
{
|
||||
// This parameter exists on Codeberg but does not seem to take effect. setting it anyway
|
||||
Some("size=128")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
url.set_query(size_query);
|
||||
}
|
||||
Ok(url)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(avatar_url)
|
||||
}
|
||||
@@ -211,7 +272,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url() {
|
||||
let parsed_remote = Codeberg
|
||||
let parsed_remote = Forgejo::public_instance()
|
||||
.parse_remote_url("git@codeberg.org:zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
@@ -226,7 +287,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_https_url() {
|
||||
let parsed_remote = Codeberg
|
||||
let parsed_remote = Forgejo::public_instance()
|
||||
.parse_remote_url("https://codeberg.org/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
@@ -239,9 +300,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_ssh_url() {
|
||||
let remote_url = "git@forgejo.my-enterprise.com:zed-industries/zed.git";
|
||||
|
||||
let parsed_remote = Forgejo::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url() {
|
||||
let remote_url = "https://forgejo.my-enterprise.com/zed-industries/zed.git";
|
||||
let parsed_remote = Forgejo::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink() {
|
||||
let permalink = Codeberg.build_permalink(
|
||||
let permalink = Forgejo::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -259,7 +355,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_with_single_line_selection() {
|
||||
let permalink = Codeberg.build_permalink(
|
||||
let permalink = Forgejo::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -277,7 +373,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_with_multi_line_selection() {
|
||||
let permalink = Codeberg.build_permalink(
|
||||
let permalink = Forgejo::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
@@ -292,4 +388,46 @@ mod tests {
|
||||
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_forgejo_self_hosted_permalink_from_ssh_url() {
|
||||
let forgejo =
|
||||
Forgejo::from_remote_url("git@forgejo.some-enterprise.com:zed-industries/zed.git")
|
||||
.unwrap();
|
||||
let permalink = forgejo.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
&repo_path("crates/editor/src/git/permalink.rs"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://forgejo.some-enterprise.com/zed-industries/zed/src/commit/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_forgejo_self_hosted_permalink_from_https_url() {
|
||||
let forgejo =
|
||||
Forgejo::from_remote_url("https://forgejo-instance.big-co.com/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
let permalink = forgejo.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
&repo_path("crates/zed/src/main.rs"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://forgejo-instance.big-co.com/zed-industries/zed/src/commit/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
}
|
||||
380
crates/git_hosting_providers/src/providers/gitea.rs
Normal file
380
crates/git_hosting_providers/src/providers/gitea.rs
Normal file
@@ -0,0 +1,380 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::SharedString;
|
||||
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use git::{
|
||||
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
|
||||
RemoteUrl,
|
||||
};
|
||||
|
||||
use crate::get_host_from_git_remote_url;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CommitDetails {
|
||||
author: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct User {
|
||||
pub avatar_url: String,
|
||||
}
|
||||
|
||||
pub struct Gitea {
|
||||
name: String,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
impl Gitea {
|
||||
pub fn new(name: impl Into<String>, base_url: Url) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_instance() -> Self {
|
||||
Self::new("Gitea", Url::parse("https://gitea.com").unwrap())
|
||||
}
|
||||
|
||||
pub fn from_remote_url(remote_url: &str) -> Result<Self> {
|
||||
let host = get_host_from_git_remote_url(remote_url)?;
|
||||
if host == "gitea.com" {
|
||||
bail!("the Gitea instance is not self-hosted");
|
||||
}
|
||||
|
||||
// TODO: detecting self hosted instances by checking whether "gitea" is in the url or not
|
||||
// is not very reliable. See https://github.com/zed-industries/zed/issues/26393 for more
|
||||
// information.
|
||||
if !host.contains("gitea") {
|
||||
bail!("not a Gitea URL");
|
||||
}
|
||||
|
||||
Ok(Self::new(
|
||||
"Gitea Self-Hosted",
|
||||
Url::parse(&format!("https://{}", host))?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn fetch_gitea_commit_author(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
repo: &str,
|
||||
commit: &str,
|
||||
client: &Arc<dyn HttpClient>,
|
||||
) -> Result<Option<User>> {
|
||||
let Some(host) = self.base_url.host_str() else {
|
||||
bail!("failed to get host from gitea base url");
|
||||
};
|
||||
let url = format!(
|
||||
"https://{host}/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}?stat=false&verification=false&files=false"
|
||||
);
|
||||
|
||||
let request = Request::get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.follow_redirects(http_client::RedirectPolicy::FollowAll);
|
||||
|
||||
let mut response = client
|
||||
.send(request.body(AsyncBody::default())?)
|
||||
.await
|
||||
.with_context(|| format!("error fetching Gitea commit details at {:?}", url))?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
|
||||
serde_json::from_str::<CommitDetails>(body_str)
|
||||
.map(|commit| commit.author)
|
||||
.context("failed to deserialize Gitea commit details")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GitHostingProvider for Gitea {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn base_url(&self) -> Url {
|
||||
self.base_url.clone()
|
||||
}
|
||||
|
||||
fn supports_avatars(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn format_line_number(&self, line: u32) -> String {
|
||||
format!("L{line}")
|
||||
}
|
||||
|
||||
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
|
||||
format!("L{start_line}-L{end_line}")
|
||||
}
|
||||
|
||||
fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
|
||||
let url = RemoteUrl::from_str(url).ok()?;
|
||||
|
||||
let host = url.host_str()?;
|
||||
if host != self.base_url.host_str()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path_segments = url.path_segments()?;
|
||||
let owner = path_segments.next()?;
|
||||
let repo = path_segments.next()?.trim_end_matches(".git");
|
||||
|
||||
Some(ParsedGitRemote {
|
||||
owner: owner.into(),
|
||||
repo: repo.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_commit_permalink(
|
||||
&self,
|
||||
remote: &ParsedGitRemote,
|
||||
params: BuildCommitPermalinkParams,
|
||||
) -> Url {
|
||||
let BuildCommitPermalinkParams { sha } = params;
|
||||
let ParsedGitRemote { owner, repo } = remote;
|
||||
|
||||
self.base_url()
|
||||
.join(&format!("{owner}/{repo}/commit/{sha}"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
|
||||
let ParsedGitRemote { owner, repo } = remote;
|
||||
let BuildPermalinkParams {
|
||||
sha,
|
||||
path,
|
||||
selection,
|
||||
} = params;
|
||||
|
||||
let mut permalink = self
|
||||
.base_url()
|
||||
.join(&format!("{owner}/{repo}/src/commit/{sha}/{path}"))
|
||||
.unwrap();
|
||||
permalink.set_fragment(
|
||||
selection
|
||||
.map(|selection| self.line_fragment(&selection))
|
||||
.as_deref(),
|
||||
);
|
||||
permalink
|
||||
}
|
||||
|
||||
async fn commit_author_avatar_url(
|
||||
&self,
|
||||
repo_owner: &str,
|
||||
repo: &str,
|
||||
commit: SharedString,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
) -> Result<Option<Url>> {
|
||||
let commit = commit.to_string();
|
||||
let avatar_url = self
|
||||
.fetch_gitea_commit_author(repo_owner, repo, &commit, &http_client)
|
||||
.await?
|
||||
.map(|author| -> Result<Url, url::ParseError> {
|
||||
let mut url = Url::parse(&author.avatar_url)?;
|
||||
if let Some(host) = url.host_str() {
|
||||
let size_query = if host.contains("gravatar") || host.contains("libravatar") {
|
||||
Some("s=128")
|
||||
} else if self
|
||||
.base_url
|
||||
.host_str()
|
||||
.is_some_and(|base_host| host.contains(base_host))
|
||||
{
|
||||
Some("size=128")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
url.set_query(size_query);
|
||||
}
|
||||
Ok(url)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(avatar_url)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use git::repository::repo_path;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url() {
|
||||
let parsed_remote = Gitea::public_instance()
|
||||
.parse_remote_url("git@gitea.com:zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_https_url() {
|
||||
let parsed_remote = Gitea::public_instance()
|
||||
.parse_remote_url("https://gitea.com/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_ssh_url() {
|
||||
let remote_url = "git@gitea.my-enterprise.com:zed-industries/zed.git";
|
||||
|
||||
let parsed_remote = Gitea::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url() {
|
||||
let remote_url = "https://gitea.my-enterprise.com/zed-industries/zed.git";
|
||||
let parsed_remote = Gitea::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink() {
|
||||
let permalink = Gitea::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
&repo_path("crates/editor/src/git/permalink.rs"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gitea.com/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_with_single_line_selection() {
|
||||
let permalink = Gitea::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
&repo_path("crates/editor/src/git/permalink.rs"),
|
||||
Some(6..6),
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gitea.com/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_codeberg_permalink_with_multi_line_selection() {
|
||||
let permalink = Gitea::public_instance().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"faa6f979be417239b2e070dbbf6392b909224e0b",
|
||||
&repo_path("crates/editor/src/git/permalink.rs"),
|
||||
Some(23..47),
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gitea.com/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitea_self_hosted_permalink_from_ssh_url() {
|
||||
let gitea =
|
||||
Gitea::from_remote_url("git@gitea.some-enterprise.com:zed-industries/zed.git").unwrap();
|
||||
let permalink = gitea.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
&repo_path("crates/editor/src/git/permalink.rs"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gitea.some-enterprise.com/zed-industries/zed/src/commit/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitea_self_hosted_permalink_from_https_url() {
|
||||
let gitea =
|
||||
Gitea::from_remote_url("https://gitea-instance.big-co.com/zed-industries/zed.git")
|
||||
.unwrap();
|
||||
let permalink = gitea.build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "zed-industries".into(),
|
||||
repo: "zed".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
&repo_path("crates/zed/src/main.rs"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gitea-instance.big-co.com/zed-industries/zed/src/commit/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
call.workspace = true
|
||||
chrono.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
|
||||
@@ -16,7 +16,6 @@ use project::{git_store::Repository, project_settings::ProjectSettings};
|
||||
use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{ContextMenu, Divider, prelude::*, tooltip_container};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -188,9 +187,11 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
.get(..8)
|
||||
.map(|sha| sha.to_string().into())
|
||||
.unwrap_or_else(|| sha.clone());
|
||||
let absolute_timestamp = format_local_timestamp(
|
||||
let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
let absolute_timestamp = time_format::format_localized_timestamp(
|
||||
commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
local_offset,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
);
|
||||
let link_color = cx.theme().colors().text_accent;
|
||||
@@ -403,11 +404,12 @@ fn deploy_blame_entry_context_menu(
|
||||
fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
let local_offset =
|
||||
time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
time::UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
local_offset,
|
||||
time_format::TimestampFormat::Relative,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use project::project_settings::ProjectSettings;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
@@ -447,9 +446,12 @@ impl PickerDelegate for BranchListDelegate {
|
||||
let subject = commit.subject.clone();
|
||||
let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc());
|
||||
let formatted_time = format_local_timestamp(
|
||||
let local_offset =
|
||||
time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
let formatted_time = time_format::format_localized_timestamp(
|
||||
commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
local_offset,
|
||||
time_format::TimestampFormat::Relative,
|
||||
);
|
||||
let author = commit.author_name.clone();
|
||||
|
||||
@@ -14,7 +14,6 @@ use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::ThemeSettings;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -190,9 +189,11 @@ impl Render for CommitTooltip {
|
||||
.map(|sha| sha.to_string().into())
|
||||
.unwrap_or_else(|| self.commit.sha.clone());
|
||||
let full_sha = self.commit.sha.to_string();
|
||||
let absolute_timestamp = format_local_timestamp(
|
||||
let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
let absolute_timestamp = time_format::format_localized_timestamp(
|
||||
self.commit.commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
local_offset,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
);
|
||||
let markdown_style = {
|
||||
@@ -351,11 +352,11 @@ impl Render for CommitTooltip {
|
||||
fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
let local_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
local_offset,
|
||||
format,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -415,12 +415,14 @@ fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
|
||||
commit.author_name, commit.author_email
|
||||
)
|
||||
.unwrap();
|
||||
let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
writeln!(
|
||||
&mut result,
|
||||
"Date: {}",
|
||||
time_format::format_local_timestamp(
|
||||
time_format::format_localized_timestamp(
|
||||
time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
|
||||
time::OffsetDateTime::now_utc(),
|
||||
local_offset,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2250,14 +2250,13 @@ impl GitPanel {
|
||||
this.askpass_delegate(format!("git pull {}", remote.name), window, cx)
|
||||
})?;
|
||||
|
||||
let branch_name = branch
|
||||
.upstream
|
||||
.is_none()
|
||||
.then(|| branch.name().to_owned().into());
|
||||
|
||||
let pull = repo.update(cx, |repo, cx| {
|
||||
repo.pull(
|
||||
branch.name().to_owned().into(),
|
||||
remote.name.clone(),
|
||||
rebase,
|
||||
askpass,
|
||||
cx,
|
||||
)
|
||||
repo.pull(branch_name, remote.name.clone(), rebase, askpass, cx)
|
||||
})?;
|
||||
|
||||
let remote_message = pull.await?;
|
||||
@@ -3619,7 +3618,7 @@ impl GitPanel {
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
.child(
|
||||
div()
|
||||
.flex_grow()
|
||||
.cursor_pointer()
|
||||
.overflow_hidden()
|
||||
.line_clamp(1)
|
||||
.child(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use chrono;
|
||||
use git::stash::StashEntry;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -207,9 +206,7 @@ impl StashListDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<StashList>,
|
||||
) -> Self {
|
||||
let timezone =
|
||||
UtcOffset::from_whole_seconds(chrono::Local::now().offset().local_minus_utc())
|
||||
.unwrap_or(UtcOffset::UTC);
|
||||
let timezone = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
|
||||
Self {
|
||||
matches: vec![],
|
||||
|
||||
@@ -114,8 +114,8 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase
|
||||
/// The imperative API equivalent of [`InteractiveElement::on_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for the given mouse button, during the bubble phase.
|
||||
/// The imperative API equivalent of [`InteractiveElement::on_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback.
|
||||
pub fn on_mouse_down(
|
||||
@@ -134,8 +134,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
||||
/// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase.
|
||||
/// The imperative API equivalent of [`InteractiveElement::capture_any_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_any_mouse_down(
|
||||
@@ -150,8 +150,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for any button, during the bubble phase
|
||||
/// the imperative API equivalent to [`InteractiveElement::on_any_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for any button, during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_any_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_any_mouse_down(
|
||||
@@ -166,8 +166,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
|
||||
/// the imperative API equivalent to [`InteractiveElement::on_mouse_up`]
|
||||
/// Bind the given callback to the mouse up event for the given button, during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_mouse_up(
|
||||
@@ -186,8 +186,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for any button, during the capture phase
|
||||
/// the imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`]
|
||||
/// Bind the given callback to the mouse up event for any button, during the capture phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_any_mouse_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_any_mouse_up(
|
||||
@@ -202,8 +202,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for any button, during the bubble phase
|
||||
/// the imperative API equivalent to [`Interactivity::on_any_mouse_up`]
|
||||
/// Bind the given callback to the mouse up event for any button, during the bubble phase.
|
||||
/// The imperative API equivalent to [`Interactivity::on_any_mouse_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_any_mouse_up(
|
||||
@@ -220,7 +220,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
|
||||
/// when the mouse is outside of the bounds of this element.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`]
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_down_out`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_mouse_down_out(
|
||||
@@ -237,7 +237,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
|
||||
/// when the mouse is outside of the bounds of this element.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`]
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_up_out`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_mouse_up_out(
|
||||
@@ -256,8 +256,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse move event, during the bubble phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_move`]
|
||||
/// Bind the given callback to the mouse move event, during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_mouse_move`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_mouse_move(
|
||||
@@ -276,7 +276,7 @@ impl Interactivity {
|
||||
/// will be called for all move events, inside or outside of this element, as long as the
|
||||
/// drag was started with this element under the mouse. Useful for implementing draggable
|
||||
/// UIs that don't conform to a drag and drop style interaction, like resizing.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_drag_move`]
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_drag_move`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_drag_move<T>(
|
||||
@@ -305,8 +305,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to scroll wheel events during the bubble phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`]
|
||||
/// Bind the given callback to scroll wheel events during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_scroll_wheel`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_scroll_wheel(
|
||||
@@ -321,8 +321,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to an action dispatch during the capture phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_action`]
|
||||
/// Bind the given callback to an action dispatch during the capture phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_action<A: Action>(
|
||||
@@ -342,8 +342,8 @@ impl Interactivity {
|
||||
));
|
||||
}
|
||||
|
||||
/// Bind the given callback to an action dispatch during the bubble phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_action`]
|
||||
/// Bind the given callback to an action dispatch during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static) {
|
||||
@@ -361,7 +361,7 @@ impl Interactivity {
|
||||
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
|
||||
/// instead of a type parameter. Useful for component libraries that want to expose
|
||||
/// action bindings to their users.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_boxed_action`]
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_boxed_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_boxed_action(
|
||||
@@ -380,8 +380,8 @@ impl Interactivity {
|
||||
));
|
||||
}
|
||||
|
||||
/// Bind the given callback to key down events during the bubble phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_down`]
|
||||
/// Bind the given callback to key down events during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_key_down(
|
||||
@@ -396,8 +396,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to key down events during the capture phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_key_down`]
|
||||
/// Bind the given callback to key down events during the capture phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::capture_key_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_key_down(
|
||||
@@ -412,8 +412,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to key up events during the bubble phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
|
||||
/// Bind the given callback to key up events during the bubble phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut Window, &mut App) + 'static) {
|
||||
@@ -425,8 +425,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to key up events during the capture phase
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_up`]
|
||||
/// Bind the given callback to key up events during the capture phase.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_key_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn capture_key_up(
|
||||
@@ -442,7 +442,7 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
/// Bind the given callback to modifiers changing events.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`]
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_modifiers_changed`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_modifiers_changed(
|
||||
@@ -455,8 +455,8 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_drop`]
|
||||
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element.
|
||||
/// The imperative API equivalent to [`InteractiveElement::on_drop`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_drop<T: 'static>(&mut self, listener: impl Fn(&T, &mut Window, &mut App) + 'static) {
|
||||
@@ -468,8 +468,8 @@ impl Interactivity {
|
||||
));
|
||||
}
|
||||
|
||||
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
|
||||
/// The imperative API equivalent to [`InteractiveElement::can_drop`]
|
||||
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element.
|
||||
/// The imperative API equivalent to [`InteractiveElement::can_drop`].
|
||||
pub fn can_drop(
|
||||
&mut self,
|
||||
predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -477,8 +477,8 @@ impl Interactivity {
|
||||
self.can_drop_predicate = Some(Box::new(predicate));
|
||||
}
|
||||
|
||||
/// Bind the given callback to click events of this element
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_click`]
|
||||
/// Bind the given callback to click events of this element.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_click`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static)
|
||||
@@ -492,8 +492,8 @@ impl Interactivity {
|
||||
|
||||
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
|
||||
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
||||
/// the [`Self::on_drag_move`] API
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]
|
||||
/// the [`Self::on_drag_move`] API.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_drag<T, W>(
|
||||
@@ -519,7 +519,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
|
||||
/// passed to the callback is true when the hover starts and false when it ends.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`]
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static)
|
||||
@@ -534,7 +534,7 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`]
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`].
|
||||
pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static)
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -551,7 +551,7 @@ impl Interactivity {
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`]
|
||||
/// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`].
|
||||
pub fn hoverable_tooltip(
|
||||
&mut self,
|
||||
build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
@@ -582,10 +582,10 @@ impl Interactivity {
|
||||
self.window_control = Some(area);
|
||||
}
|
||||
|
||||
/// Block non-scroll mouse interactions with elements behind this element's hitbox. See
|
||||
/// [`Hitbox::is_hovered`] for details.
|
||||
/// Block non-scroll mouse interactions with elements behind this element's hitbox.
|
||||
/// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`].
|
||||
///
|
||||
/// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]
|
||||
/// See [`Hitbox::is_hovered`] for details.
|
||||
pub fn block_mouse_except_scroll(&mut self) {
|
||||
self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll;
|
||||
}
|
||||
@@ -689,8 +689,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for the given mouse button,
|
||||
/// the fluent API equivalent to [`Interactivity::on_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for the given mouse button.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to the view state from this callback.
|
||||
fn on_mouse_down(
|
||||
@@ -720,8 +720,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
||||
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_any_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_any_mouse_down(
|
||||
@@ -732,8 +732,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase
|
||||
/// the fluent API equivalent to [`Interactivity::on_any_mouse_down`]
|
||||
/// Bind the given callback to the mouse down event for any button, during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_any_mouse_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_any_mouse_down(
|
||||
@@ -744,8 +744,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for the given button, during the bubble phase
|
||||
/// the fluent API equivalent to [`Interactivity::on_mouse_up`]
|
||||
/// Bind the given callback to the mouse up event for the given button, during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_mouse_up(
|
||||
@@ -757,8 +757,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse up event for any button, during the capture phase
|
||||
/// the fluent API equivalent to [`Interactivity::capture_any_mouse_up`]
|
||||
/// Bind the given callback to the mouse up event for any button, during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_any_mouse_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_any_mouse_up(
|
||||
@@ -771,7 +771,7 @@ pub trait InteractiveElement: Sized {
|
||||
|
||||
/// Bind the given callback to the mouse down event, on any button, during the capture phase,
|
||||
/// when the mouse is outside of the bounds of this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_down_out`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_mouse_down_out(
|
||||
@@ -784,7 +784,7 @@ pub trait InteractiveElement: Sized {
|
||||
|
||||
/// Bind the given callback to the mouse up event, for the given button, during the capture phase,
|
||||
/// when the mouse is outside of the bounds of this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_up_out`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_up_out`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_mouse_up_out(
|
||||
@@ -796,8 +796,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to the mouse move event, during the bubble phase
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_move`]
|
||||
/// Bind the given callback to the mouse move event, during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_mouse_move`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_mouse_move(
|
||||
@@ -812,7 +812,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// will be called for all move events, inside or outside of this element, as long as the
|
||||
/// drag was started with this element under the mouse. Useful for implementing draggable
|
||||
/// UIs that don't conform to a drag and drop style interaction, like resizing.
|
||||
/// The fluent API equivalent to [`Interactivity::on_drag_move`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_drag_move`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_drag_move<T: 'static>(
|
||||
@@ -823,8 +823,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to scroll wheel events during the bubble phase
|
||||
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
|
||||
/// Bind the given callback to scroll wheel events during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_scroll_wheel(
|
||||
@@ -835,8 +835,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Capture the given action, before normal action dispatch can fire
|
||||
/// The fluent API equivalent to [`Interactivity::on_scroll_wheel`]
|
||||
/// Capture the given action, before normal action dispatch can fire.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_action<A: Action>(
|
||||
@@ -847,8 +847,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to an action dispatch during the bubble phase
|
||||
/// The fluent API equivalent to [`Interactivity::on_action`]
|
||||
/// Bind the given callback to an action dispatch during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_action<A: Action>(
|
||||
@@ -862,7 +862,7 @@ pub trait InteractiveElement: Sized {
|
||||
/// Bind the given callback to an action dispatch, based on a dynamic action parameter
|
||||
/// instead of a type parameter. Useful for component libraries that want to expose
|
||||
/// action bindings to their users.
|
||||
/// The fluent API equivalent to [`Interactivity::on_boxed_action`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_boxed_action`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_boxed_action(
|
||||
@@ -874,8 +874,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to key down events during the bubble phase
|
||||
/// The fluent API equivalent to [`Interactivity::on_key_down`]
|
||||
/// Bind the given callback to key down events during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_key_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_key_down(
|
||||
@@ -886,8 +886,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to key down events during the capture phase
|
||||
/// The fluent API equivalent to [`Interactivity::capture_key_down`]
|
||||
/// Bind the given callback to key down events during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_key_down`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_key_down(
|
||||
@@ -898,8 +898,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to key up events during the bubble phase
|
||||
/// The fluent API equivalent to [`Interactivity::on_key_up`]
|
||||
/// Bind the given callback to key up events during the bubble phase.
|
||||
/// The fluent API equivalent to [`Interactivity::on_key_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_key_up(
|
||||
@@ -910,8 +910,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to key up events during the capture phase
|
||||
/// The fluent API equivalent to [`Interactivity::capture_key_up`]
|
||||
/// Bind the given callback to key up events during the capture phase.
|
||||
/// The fluent API equivalent to [`Interactivity::capture_key_up`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn capture_key_up(
|
||||
@@ -923,7 +923,7 @@ pub trait InteractiveElement: Sized {
|
||||
}
|
||||
|
||||
/// Bind the given callback to modifiers changing events.
|
||||
/// The fluent API equivalent to [`Interactivity::on_modifiers_changed`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_modifiers_changed`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_modifiers_changed(
|
||||
@@ -969,8 +969,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element
|
||||
/// The fluent API equivalent to [`Interactivity::on_drop`]
|
||||
/// Bind the given callback to drop events of the given type, whether or not the drag started on this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_drop`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_drop<T: 'static>(
|
||||
@@ -981,8 +981,8 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element
|
||||
/// The fluent API equivalent to [`Interactivity::can_drop`]
|
||||
/// Use the given predicate to determine whether or not a drop event should be dispatched to this element.
|
||||
/// The fluent API equivalent to [`Interactivity::can_drop`].
|
||||
fn can_drop(
|
||||
mut self,
|
||||
predicate: impl Fn(&dyn Any, &mut Window, &mut App) -> bool + 'static,
|
||||
@@ -993,23 +993,23 @@ pub trait InteractiveElement: Sized {
|
||||
|
||||
/// Block the mouse from all interactions with elements behind this element's hitbox. Typically
|
||||
/// `block_mouse_except_scroll` should be preferred.
|
||||
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
|
||||
/// The fluent API equivalent to [`Interactivity::occlude_mouse`].
|
||||
fn occlude(mut self) -> Self {
|
||||
self.interactivity().occlude_mouse();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bounds of this element as a window control area for the platform window.
|
||||
/// The fluent API equivalent to [`Interactivity::window_control_area`]
|
||||
/// The fluent API equivalent to [`Interactivity::window_control_area`].
|
||||
fn window_control_area(mut self, area: WindowControlArea) -> Self {
|
||||
self.interactivity().window_control_area(area);
|
||||
self
|
||||
}
|
||||
|
||||
/// Block non-scroll mouse interactions with elements behind this element's hitbox. See
|
||||
/// [`Hitbox::is_hovered`] for details.
|
||||
/// Block non-scroll mouse interactions with elements behind this element's hitbox.
|
||||
/// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`].
|
||||
///
|
||||
/// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]
|
||||
/// See [`Hitbox::is_hovered`] for details.
|
||||
fn block_mouse_except_scroll(mut self) -> Self {
|
||||
self.interactivity().block_mouse_except_scroll();
|
||||
self
|
||||
@@ -1122,8 +1122,8 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind the given callback to click events of this element
|
||||
/// The fluent API equivalent to [`Interactivity::on_click`]
|
||||
/// Bind the given callback to click events of this element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_click`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self
|
||||
@@ -1138,7 +1138,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
|
||||
/// the [`InteractiveElement::on_drag_move`] API.
|
||||
/// The callback also has access to the offset of triggering click from the origin of parent element.
|
||||
/// The fluent API equivalent to [`Interactivity::on_drag`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_drag`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_drag<T, W>(
|
||||
@@ -1157,7 +1157,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
|
||||
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
|
||||
/// passed to the callback is true when the hover starts and false when it ends.
|
||||
/// The fluent API equivalent to [`Interactivity::on_hover`]
|
||||
/// The fluent API equivalent to [`Interactivity::on_hover`].
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self
|
||||
@@ -1169,7 +1169,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The fluent API equivalent to [`Interactivity::tooltip`]
|
||||
/// The fluent API equivalent to [`Interactivity::tooltip`].
|
||||
fn tooltip(mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -1180,7 +1180,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]
|
||||
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`].
|
||||
fn hoverable_tooltip(
|
||||
mut self,
|
||||
build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ impl Default for NavigationDirection {
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse move event from the platform
|
||||
/// A mouse move event from the platform.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseMoveEvent {
|
||||
/// The position of the mouse on the window.
|
||||
@@ -383,7 +383,7 @@ impl MouseMoveEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse wheel event from the platform
|
||||
/// A mouse wheel event from the platform.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
/// The position of the mouse on the window.
|
||||
|
||||
@@ -572,6 +572,14 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with just function.
|
||||
pub fn function() -> Modifiers {
|
||||
Modifiers {
|
||||
function: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Modifiers`] with command + shift.
|
||||
pub fn command_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
|
||||
@@ -1124,7 +1124,32 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string, try the various supported image types.
|
||||
// Next, check for URL flavors (including file URLs). Some tools only provide a URL
|
||||
// with no plain text entry.
|
||||
{
|
||||
// Try the modern UTType identifiers first.
|
||||
let file_url_type: id = ns_string("public.file-url");
|
||||
let url_type: id = ns_string("public.url");
|
||||
|
||||
let url_data = if msg_send![types, containsObject: file_url_type] {
|
||||
pasteboard.dataForType(file_url_type)
|
||||
} else if msg_send![types, containsObject: url_type] {
|
||||
pasteboard.dataForType(url_type)
|
||||
} else {
|
||||
nil
|
||||
};
|
||||
|
||||
if url_data != nil && !url_data.bytes().is_null() {
|
||||
let bytes = slice::from_raw_parts(
|
||||
url_data.bytes() as *mut u8,
|
||||
url_data.length() as usize,
|
||||
);
|
||||
|
||||
return Some(self.read_string_from_clipboard(&state, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or URL, try the various supported image types.
|
||||
for format in ImageFormat::iter() {
|
||||
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
||||
return Some(item);
|
||||
@@ -1132,7 +1157,7 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or a supported image type, give up.
|
||||
// If it wasn't a string, URL, or a supported image type, give up.
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1707,6 +1732,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_url_reads_as_url_string() {
|
||||
let platform = build_platform();
|
||||
|
||||
// Create a file URL for an arbitrary test path and write it to the pasteboard.
|
||||
// This path does not need to exist; we only validate URL→path conversion.
|
||||
let mock_path = "/tmp/zed-clipboard-file-url-test";
|
||||
unsafe {
|
||||
// Build an NSURL from the file path
|
||||
let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(mock_path)];
|
||||
let abs: id = msg_send![url, absoluteString];
|
||||
|
||||
// Encode the URL string as UTF-8 bytes
|
||||
let len: usize = msg_send![abs, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||
let bytes_ptr = abs.UTF8String() as *const u8;
|
||||
let data = NSData::dataWithBytes_length_(nil, bytes_ptr as *const c_void, len as u64);
|
||||
|
||||
// Write as public.file-url to the unique pasteboard
|
||||
let file_url_type: id = ns_string("public.file-url");
|
||||
platform
|
||||
.0
|
||||
.lock()
|
||||
.pasteboard
|
||||
.setData_forType(data, file_url_type);
|
||||
}
|
||||
|
||||
// Ensure the clipboard read returns the URL string, not a converted path
|
||||
let expected_url = format!("file://{}", mock_path);
|
||||
assert_eq!(
|
||||
platform.read_from_clipboard(),
|
||||
Some(ClipboardItem::new_string(expected_url))
|
||||
);
|
||||
}
|
||||
|
||||
fn build_platform() -> MacPlatform {
|
||||
let platform = MacPlatform::new(false);
|
||||
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||
|
||||
@@ -1753,9 +1753,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
}
|
||||
}
|
||||
|
||||
// Don't send key equivalents to the input handler,
|
||||
// or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent {
|
||||
// Don't send key equivalents to the input handler if there are key modifiers other
|
||||
// than Function key, or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent && key_down_event.keystroke.modifiers != Modifiers::function() {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(¶ms.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(¶ms.path)? {
|
||||
render_pixmap(&bytes)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
|
||||
|
||||
@@ -739,7 +739,7 @@ impl Display for FontStyle {
|
||||
}
|
||||
|
||||
/// A styled run of text, for use in [`crate::TextLayout`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub struct TextRun {
|
||||
/// A number of utf8 bytes
|
||||
pub len: usize,
|
||||
@@ -813,6 +813,12 @@ pub struct Font {
|
||||
pub style: FontStyle,
|
||||
}
|
||||
|
||||
impl Default for Font {
|
||||
fn default() -> Self {
|
||||
font(".SystemUIFont")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a [`Font`] for a given name.
|
||||
pub fn font(family: impl Into<SharedString>) -> Font {
|
||||
Font {
|
||||
|
||||
@@ -315,9 +315,7 @@ impl Boundary {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, font,
|
||||
};
|
||||
use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font};
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::{TextRun, WindowTextSystem, WrapBoundary};
|
||||
use rand::prelude::*;
|
||||
@@ -341,10 +339,7 @@ mod tests {
|
||||
weight: FontWeight::default(),
|
||||
style: FontStyle::Normal,
|
||||
},
|
||||
color: Hsla::default(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -691,16 +686,12 @@ mod tests {
|
||||
font: font("Helvetica"),
|
||||
color: Default::default(),
|
||||
underline: Default::default(),
|
||||
strikethrough: None,
|
||||
background_color: None,
|
||||
..Default::default()
|
||||
};
|
||||
let bold = TextRun {
|
||||
len: 0,
|
||||
font: font("Helvetica").bold(),
|
||||
color: Default::default(),
|
||||
underline: Default::default(),
|
||||
strikethrough: None,
|
||||
background_color: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let text = "aa bbb cccc ddddd eeee".into();
|
||||
|
||||
@@ -918,26 +918,85 @@ pub(crate) struct ElementStateBox {
|
||||
}
|
||||
|
||||
fn default_bounds(display_id: Option<DisplayId>, cx: &mut App) -> Bounds<Pixels> {
|
||||
const DEFAULT_WINDOW_OFFSET: Point<Pixels> = point(px(0.), px(35.));
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
const CASCADE_OFFSET: f32 = 25.0;
|
||||
|
||||
// TODO, BUG: if you open a window with the currently active window
|
||||
// on the stack, this will erroneously select the 'unwrap_or_else'
|
||||
// code path
|
||||
cx.active_window()
|
||||
.and_then(|w| w.update(cx, |_, window, _| window.bounds()).ok())
|
||||
.map(|mut bounds| {
|
||||
bounds.origin += DEFAULT_WINDOW_OFFSET;
|
||||
bounds
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let display = display_id
|
||||
.map(|id| cx.find_display(id))
|
||||
.unwrap_or_else(|| cx.primary_display());
|
||||
let display = display_id
|
||||
.map(|id| cx.find_display(id))
|
||||
.unwrap_or_else(|| cx.primary_display());
|
||||
|
||||
display
|
||||
.map(|display| display.default_bounds())
|
||||
.unwrap_or_else(|| Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE))
|
||||
})
|
||||
let display_bounds = display
|
||||
.as_ref()
|
||||
.map(|d| d.bounds())
|
||||
.unwrap_or_else(|| Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE));
|
||||
|
||||
// TODO, BUG: if you open a window with the currently active window
|
||||
// on the stack, this will erroneously select the 'unwrap_or_else'
|
||||
// code path
|
||||
let (base_origin, base_size) = cx
|
||||
.active_window()
|
||||
.and_then(|w| {
|
||||
w.update(cx, |_, window, _| {
|
||||
let bounds = window.bounds();
|
||||
(bounds.origin, bounds.size)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let default_bounds = display
|
||||
.as_ref()
|
||||
.map(|d| d.default_bounds())
|
||||
.unwrap_or_else(|| Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE));
|
||||
(default_bounds.origin, default_bounds.size)
|
||||
});
|
||||
|
||||
let cascade_offset = point(px(CASCADE_OFFSET), px(CASCADE_OFFSET));
|
||||
let proposed_origin = base_origin + cascade_offset;
|
||||
let proposed_bounds = Bounds::new(proposed_origin, base_size);
|
||||
|
||||
let display_right = display_bounds.origin.x + display_bounds.size.width;
|
||||
let display_bottom = display_bounds.origin.y + display_bounds.size.height;
|
||||
let window_right = proposed_bounds.origin.x + proposed_bounds.size.width;
|
||||
let window_bottom = proposed_bounds.origin.y + proposed_bounds.size.height;
|
||||
|
||||
let fits_horizontally = window_right <= display_right;
|
||||
let fits_vertically = window_bottom <= display_bottom;
|
||||
|
||||
let final_origin = match (fits_horizontally, fits_vertically) {
|
||||
(true, true) => proposed_origin,
|
||||
(false, true) => point(display_bounds.origin.x, base_origin.y),
|
||||
(true, false) => point(base_origin.x, display_bounds.origin.y),
|
||||
(false, false) => display_bounds.origin,
|
||||
};
|
||||
|
||||
Bounds::new(final_origin, base_size)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
const DEFAULT_WINDOW_OFFSET: Point<Pixels> = point(px(0.), px(35.));
|
||||
|
||||
// TODO, BUG: if you open a window with the currently active window
|
||||
// on the stack, this will erroneously select the 'unwrap_or_else'
|
||||
// code path
|
||||
cx.active_window()
|
||||
.and_then(|w| w.update(cx, |_, window, _| window.bounds()).ok())
|
||||
.map(|mut bounds| {
|
||||
bounds.origin += DEFAULT_WINDOW_OFFSET;
|
||||
bounds
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let display = display_id
|
||||
.map(|id| cx.find_display(id))
|
||||
.unwrap_or_else(|| cx.primary_display());
|
||||
|
||||
display
|
||||
.as_ref()
|
||||
.map(|display| display.default_bounds())
|
||||
.unwrap_or_else(|| Bounds::new(point(px(0.), px(0.)), DEFAULT_WINDOW_SIZE))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
@@ -1862,6 +1921,9 @@ impl Window {
|
||||
/// Executes the provided function with the specified rem size.
|
||||
///
|
||||
/// This method must only be called as part of element drawing.
|
||||
// This function is called in a highly recursive manner in editor
|
||||
// prepainting, make sure its inlined to reduce the stack burden
|
||||
#[inline]
|
||||
pub fn with_rem_size<F, R>(&mut self, rem_size: Option<impl Into<Pixels>>, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
@@ -2351,6 +2413,9 @@ impl Window {
|
||||
/// Push a text style onto the stack, and call a function with that style active.
|
||||
/// Use [`Window::text_style`] to get the current, combined text style. This method
|
||||
/// should only be called as part of element drawing.
|
||||
// This function is called in a highly recursive manner in editor
|
||||
// prepainting, make sure its inlined to reduce the stack burden
|
||||
#[inline]
|
||||
pub fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
@@ -2401,6 +2466,9 @@ impl Window {
|
||||
|
||||
/// Invoke the given function with the given content mask after intersecting it
|
||||
/// with the current mask. This method should only be called during element drawing.
|
||||
// This function is called in a highly recursive manner in editor
|
||||
// prepainting, make sure its inlined to reduce the stack burden
|
||||
#[inline]
|
||||
pub fn with_content_mask<R>(
|
||||
&mut self,
|
||||
mask: Option<ContentMask<Pixels>>,
|
||||
@@ -3084,6 +3152,7 @@ impl Window {
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
path: SharedString,
|
||||
mut data: Option<&[u8]>,
|
||||
transformation: TransformationMatrix,
|
||||
color: Hsla,
|
||||
cx: &App,
|
||||
@@ -3104,7 +3173,8 @@ impl Window {
|
||||
let Some(tile) =
|
||||
self.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms)? else {
|
||||
let Some((size, bytes)) = cx.svg_renderer.render_alpha_mask(¶ms, data)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some((size, Cow::Owned(bytes))))
|
||||
@@ -3435,25 +3505,25 @@ impl Window {
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn on_mouse_event<Event: MouseEvent>(
|
||||
&mut self,
|
||||
mut handler: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static,
|
||||
mut listener: impl FnMut(&Event, DispatchPhase, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
|
||||
self.next_frame.mouse_listeners.push(Some(Box::new(
|
||||
move |event: &dyn Any, phase: DispatchPhase, window: &mut Window, cx: &mut App| {
|
||||
if let Some(event) = event.downcast_ref() {
|
||||
handler(event, phase, window, cx)
|
||||
listener(event, phase, window, cx)
|
||||
}
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
/// Register a key event listener on the window for the next frame. The type of event
|
||||
/// Register a key event listener on this node for the next frame. The type of event
|
||||
/// is determined by the first parameter of the given listener. When the next frame is rendered
|
||||
/// the listener will be cleared.
|
||||
///
|
||||
/// This is a fairly low-level method, so prefer using event handlers on elements unless you have
|
||||
/// a specific need to register a global listener.
|
||||
/// a specific need to register a listener yourself.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn on_key_event<Event: KeyEvent>(
|
||||
@@ -4382,34 +4452,42 @@ impl Window {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Register an action listener on the window for the next frame. The type of action
|
||||
/// Register an action listener on this node for the next frame. The type of action
|
||||
/// is determined by the first parameter of the given listener. When the next frame is rendered
|
||||
/// the listener will be cleared.
|
||||
///
|
||||
/// This is a fairly low-level method, so prefer using action handlers on elements unless you have
|
||||
/// a specific need to register a global listener.
|
||||
/// a specific need to register a listener yourself.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
|
||||
self.next_frame
|
||||
.dispatch_tree
|
||||
.on_action(action_type, Rc::new(listener));
|
||||
}
|
||||
|
||||
/// Register an action listener on the window for the next frame if the condition is true.
|
||||
/// The type of action is determined by the first parameter of the given listener.
|
||||
/// When the next frame is rendered the listener will be cleared.
|
||||
/// Register a capturing action listener on this node for the next frame if the condition is true.
|
||||
/// The type of action is determined by the first parameter of the given listener. When the next
|
||||
/// frame is rendered the listener will be cleared.
|
||||
///
|
||||
/// This is a fairly low-level method, so prefer using action handlers on elements unless you have
|
||||
/// a specific need to register a global listener.
|
||||
/// a specific need to register a listener yourself.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
pub fn on_action_when(
|
||||
&mut self,
|
||||
condition: bool,
|
||||
action_type: TypeId,
|
||||
listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static,
|
||||
) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
|
||||
if condition {
|
||||
self.next_frame
|
||||
.dispatch_tree
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)],
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)],
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1215,7 +1215,7 @@ impl Element for MarkdownElement {
|
||||
}
|
||||
MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
|
||||
MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
|
||||
_ => log::error!("unsupported markdown event {:?}", event),
|
||||
_ => log::debug!("unsupported markdown event {:?}", event),
|
||||
}
|
||||
}
|
||||
let mut rendered_markdown = builder.build();
|
||||
|
||||
@@ -376,7 +376,7 @@ struct ManagedNodeRuntime {
|
||||
}
|
||||
|
||||
impl ManagedNodeRuntime {
|
||||
const VERSION: &str = "v22.5.1";
|
||||
const VERSION: &str = "v24.11.0";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NODE_PATH: &str = "bin/node";
|
||||
|
||||
@@ -5204,6 +5204,9 @@ fn subscribe_for_editor_events(
|
||||
outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
|
||||
}
|
||||
}
|
||||
EditorEvent::TitleChanged => {
|
||||
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -130,9 +130,17 @@ impl ContextServerConfiguration {
|
||||
.ok()
|
||||
.flatten()?;
|
||||
|
||||
let command = descriptor.command(worktree_store, cx).await.log_err()?;
|
||||
|
||||
Some(ContextServerConfiguration::Extension { command, settings })
|
||||
match descriptor.command(worktree_store, cx).await {
|
||||
Ok(command) => {
|
||||
Some(ContextServerConfiguration::Extension { command, settings })
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to create context server configuration from settings: {e:#}"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1723,7 +1723,7 @@ impl GitStore {
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
let branch_name = envelope.payload.branch_name.into();
|
||||
let branch_name = envelope.payload.branch_name.map(|name| name.into());
|
||||
let remote_name = envelope.payload.remote_name.into();
|
||||
let rebase = envelope.payload.rebase;
|
||||
|
||||
@@ -4293,7 +4293,7 @@ impl Repository {
|
||||
|
||||
pub fn pull(
|
||||
&mut self,
|
||||
branch: SharedString,
|
||||
branch: Option<SharedString>,
|
||||
remote: SharedString,
|
||||
rebase: bool,
|
||||
askpass: AskPassDelegate,
|
||||
@@ -4303,13 +4303,16 @@ impl Repository {
|
||||
let askpass_id = util::post_inc(&mut self.latest_askpass_id);
|
||||
let id = self.id;
|
||||
|
||||
let status = if rebase {
|
||||
Some(format!("git pull --rebase {} {}", remote, branch).into())
|
||||
} else {
|
||||
Some(format!("git pull {} {}", remote, branch).into())
|
||||
};
|
||||
let mut status = "git pull".to_string();
|
||||
if rebase {
|
||||
status.push_str(" --rebase");
|
||||
}
|
||||
status.push_str(&format!(" {}", remote));
|
||||
if let Some(b) = &branch {
|
||||
status.push_str(&format!(" {}", b));
|
||||
}
|
||||
|
||||
self.send_job(status, move |git_repo, cx| async move {
|
||||
self.send_job(Some(status.into()), move |git_repo, cx| async move {
|
||||
match git_repo {
|
||||
RepositoryState::Local {
|
||||
backend,
|
||||
@@ -4318,7 +4321,7 @@ impl Repository {
|
||||
} => {
|
||||
backend
|
||||
.pull(
|
||||
branch.to_string(),
|
||||
branch.as_ref().map(|b| b.to_string()),
|
||||
remote.to_string(),
|
||||
rebase,
|
||||
askpass,
|
||||
@@ -4339,7 +4342,7 @@ impl Repository {
|
||||
repository_id: id.to_proto(),
|
||||
askpass_id,
|
||||
rebase,
|
||||
branch_name: branch.to_string(),
|
||||
branch_name: branch.as_ref().map(|b| b.to_string()),
|
||||
remote_name: remote.to_string(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -344,7 +344,7 @@ pub enum Event {
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
|
||||
EntryRenamed(ProjectTransaction),
|
||||
EntryRenamed(ProjectTransaction, ProjectPath, PathBuf),
|
||||
AgentLocationChanged,
|
||||
}
|
||||
|
||||
@@ -2248,7 +2248,11 @@ impl Project {
|
||||
|
||||
project
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(Event::EntryRenamed(transaction));
|
||||
cx.emit(Event::EntryRenamed(
|
||||
transaction,
|
||||
new_path.clone(),
|
||||
new_abs_path.clone(),
|
||||
));
|
||||
})
|
||||
.ok();
|
||||
|
||||
|
||||
@@ -2199,6 +2199,396 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: This test is skipped on Windows, because on Windows,
|
||||
// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri
|
||||
// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI`
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.txt": "// First Txt file",
|
||||
"second.txt": "// Second Txt file",
|
||||
"third.txt": "// Third Txt file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let panel = ProjectPanel::new(workspace, window, cx);
|
||||
workspace.add_panel(panel.clone(), window, cx);
|
||||
panel
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
select_path(&panel, "src", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src <== selected",
|
||||
" > test"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.new_directory(&NewDirectory, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(panel.filename_editor.read(cx).is_focused(window));
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src",
|
||||
" > [EDITOR: ''] <== selected",
|
||||
" > test"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("test", window, cx));
|
||||
assert!(
|
||||
panel.confirm_edit(true, window, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new directory name"
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.state.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting new directory name"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
panel.update_visible_entries(None, false, false, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src <== selected",
|
||||
" > test"
|
||||
],
|
||||
"File list should be unchanged after failed folder create confirmation"
|
||||
);
|
||||
|
||||
select_path(&panel, "src/test", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src",
|
||||
" > test <== selected"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
|
||||
cx.run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(panel.filename_editor.read(cx).is_focused(window));
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" [EDITOR: ''] <== selected",
|
||||
" first.txt",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("first.txt", window, cx));
|
||||
assert!(
|
||||
panel.confirm_edit(true, window, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new file name"
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.state.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting new file name"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
panel.update_visible_entries(None, false, false, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test <== selected",
|
||||
" first.txt",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
"File list should be unchanged after failed file create confirmation"
|
||||
);
|
||||
|
||||
select_path(&panel, "src/test/first.txt", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.txt <== selected",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(panel.filename_editor.read(cx).is_focused(window));
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" [EDITOR: 'first.txt'] <== selected",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("second.txt", window, cx));
|
||||
assert!(
|
||||
panel.confirm_edit(true, window, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting file rename"
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.state.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting file rename"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.txt <== selected",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
"File list should be unchanged after failed rename confirmation"
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
// Try to duplicate and check history
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.duplicate(&Duplicate, window, cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.txt",
|
||||
" [EDITOR: 'first copy.txt'] <== selected <== marked",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
);
|
||||
|
||||
let confirm = panel.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
|
||||
panel.confirm_edit(true, window, cx).unwrap()
|
||||
});
|
||||
confirm.await.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.txt",
|
||||
" fourth.txt <== selected",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
"File list should be different after rename confirmation"
|
||||
);
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.update_visible_entries(None, false, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
select_path(&panel, "src/test/first.txt", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
workspace
|
||||
.read_with(cx, |this, cx| {
|
||||
assert!(
|
||||
this.recent_navigation_history_iter(cx)
|
||||
.any(|(project_path, abs_path)| {
|
||||
project_path.path == Arc::from(rel_path("test/fourth.txt"))
|
||||
&& abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
|
||||
})
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// NOTE: This test is skipped on Windows, because on Windows,
|
||||
// when it triggers the lsp store it converts `/src/test/first.txt` into an uri
|
||||
// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI`
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.txt": "// First Txt file",
|
||||
"second.txt": "// Second Txt file",
|
||||
"third.txt": "// Third Txt file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let panel = ProjectPanel::new(workspace, window, cx);
|
||||
workspace.add_panel(panel.clone(), window, cx);
|
||||
panel
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
select_path(&panel, "src", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src <== selected",
|
||||
" > test"
|
||||
]
|
||||
);
|
||||
|
||||
select_path(&panel, "src/test", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src",
|
||||
" > test <== selected"
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
|
||||
cx.run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(panel.filename_editor.read(cx).is_focused(window));
|
||||
});
|
||||
|
||||
select_path(&panel, "src/test/first.txt", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" [EDITOR: 'first.txt'] <== selected <== marked",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
);
|
||||
|
||||
let confirm = panel.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.filename_editor
|
||||
.update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx));
|
||||
panel.confirm_edit(true, window, cx).unwrap()
|
||||
});
|
||||
confirm.await.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" fourth.txt <== selected",
|
||||
" second.txt",
|
||||
" third.txt"
|
||||
],
|
||||
"File list should be different after rename confirmation"
|
||||
);
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.update_visible_entries(None, false, false, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
select_path(&panel, "src/test/second.txt", cx);
|
||||
panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
workspace
|
||||
.read_with(cx, |this, cx| {
|
||||
assert!(
|
||||
this.recent_navigation_history_iter(cx)
|
||||
.any(|(project_path, abs_path)| {
|
||||
project_path.path == Arc::from(rel_path("test/fourth.txt"))
|
||||
&& abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt")))
|
||||
})
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_git_entry(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
@@ -403,7 +403,7 @@ message Pull {
|
||||
reserved 2;
|
||||
uint64 repository_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
optional string branch_name = 5;
|
||||
uint64 askpass_id = 6;
|
||||
bool rebase = 7;
|
||||
}
|
||||
|
||||
@@ -482,12 +482,14 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate {
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<anyhow::Result<PathBuf>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
AutoUpdater::download_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
version,
|
||||
move |status, cx| this.set_status(Some(status), cx),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
@@ -659,7 +661,7 @@ pub async fn open_remote_project(
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
log::error!("Failed to open project: {e:?}");
|
||||
log::error!("Failed to open project: {e:#}");
|
||||
let response = window
|
||||
.update(cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
@@ -668,7 +670,7 @@ pub async fn open_remote_project(
|
||||
RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
|
||||
RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
|
||||
},
|
||||
Some(&e.to_string()),
|
||||
Some(&format!("{e:#}")),
|
||||
&["Retry", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
@@ -715,7 +717,7 @@ pub async fn open_remote_project(
|
||||
|
||||
match opened_items {
|
||||
Err(e) => {
|
||||
log::error!("Failed to open project: {e:?}");
|
||||
log::error!("Failed to open project: {e:#}");
|
||||
let response = window
|
||||
.update(cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
@@ -724,7 +726,7 @@ pub async fn open_remote_project(
|
||||
RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
|
||||
RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
|
||||
},
|
||||
Some(&e.to_string()),
|
||||
Some(&format!("{e:#}")),
|
||||
&["Retry", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -370,7 +370,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
|
||||
let ssh_proxy_process = match self
|
||||
.socket
|
||||
.ssh_command(self.ssh_shell_kind, "env", &proxy_args)
|
||||
.ssh_command(self.ssh_shell_kind, "env", &proxy_args, false)
|
||||
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
@@ -578,6 +578,7 @@ impl SshRemoteConnection {
|
||||
self.ssh_shell_kind,
|
||||
&dst_path.display(self.path_style()),
|
||||
&["version"],
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
@@ -615,13 +616,13 @@ impl SshRemoteConnection {
|
||||
{
|
||||
Ok(_) => {
|
||||
self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
|
||||
.await?;
|
||||
.await
|
||||
.context("extracting server binary")?;
|
||||
return Ok(dst_path);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to download binary on server, attempting to upload server: {}",
|
||||
e
|
||||
"Failed to download binary on server, attempting to upload server: {e:#}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -629,11 +630,14 @@ impl SshRemoteConnection {
|
||||
|
||||
let src_path = delegate
|
||||
.download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx)
|
||||
.await?;
|
||||
.await
|
||||
.context("downloading server binary locally")?;
|
||||
self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx)
|
||||
.await?;
|
||||
.await
|
||||
.context("uploading server binary")?;
|
||||
self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx)
|
||||
.await?;
|
||||
.await
|
||||
.context("extracting server binary")?;
|
||||
Ok(dst_path)
|
||||
}
|
||||
|
||||
@@ -651,6 +655,7 @@ impl SshRemoteConnection {
|
||||
self.ssh_shell_kind,
|
||||
"mkdir",
|
||||
&["-p", parent.display(self.path_style()).as_ref()],
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -675,6 +680,7 @@ impl SshRemoteConnection {
|
||||
"-o",
|
||||
&tmp_path_gz.display(self.path_style()),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -682,7 +688,7 @@ impl SshRemoteConnection {
|
||||
Err(e) => {
|
||||
if self
|
||||
.socket
|
||||
.run_command(self.ssh_shell_kind, "which", &["curl"])
|
||||
.run_command(self.ssh_shell_kind, "which", &["curl"], true)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -702,6 +708,7 @@ impl SshRemoteConnection {
|
||||
"-O",
|
||||
&tmp_path_gz.display(self.path_style()),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -709,7 +716,7 @@ impl SshRemoteConnection {
|
||||
Err(e) => {
|
||||
if self
|
||||
.socket
|
||||
.run_command(self.ssh_shell_kind, "which", &["wget"])
|
||||
.run_command(self.ssh_shell_kind, "which", &["wget"], true)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -738,6 +745,7 @@ impl SshRemoteConnection {
|
||||
self.ssh_shell_kind,
|
||||
"mkdir",
|
||||
&["-p", parent.display(self.path_style()).as_ref()],
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -778,14 +786,23 @@ impl SshRemoteConnection {
|
||||
let dst_path = dst_path.display(self.path_style());
|
||||
let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?;
|
||||
let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") {
|
||||
let orig_tmp_path = shell_kind
|
||||
.try_quote(&orig_tmp_path)
|
||||
.context("shell quoting")?;
|
||||
let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?;
|
||||
format!(
|
||||
"gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}",
|
||||
)
|
||||
} else {
|
||||
let orig_tmp_path = shell_kind
|
||||
.try_quote(&orig_tmp_path)
|
||||
.context("shell quoting")?;
|
||||
format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",)
|
||||
};
|
||||
let args = shell_kind.args_for_shell(false, script.to_string());
|
||||
self.socket.run_command(shell_kind, "sh", &args).await?;
|
||||
self.socket
|
||||
.run_command(shell_kind, "sh", &args, true)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -850,7 +867,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?;
|
||||
@@ -934,6 +951,7 @@ impl SshSocket {
|
||||
shell_kind: ShellKind,
|
||||
program: &str,
|
||||
args: &[impl AsRef<str>],
|
||||
allow_pseudo_tty: bool,
|
||||
) -> process::Command {
|
||||
let mut command = util::command::new_smol_command("ssh");
|
||||
let program = shell_kind.prepend_command_prefix(program);
|
||||
@@ -953,9 +971,11 @@ impl SshSocket {
|
||||
let separator = shell_kind.sequential_commands_separator();
|
||||
let to_run = format!("cd{separator} {to_run}");
|
||||
self.ssh_options(&mut command, true)
|
||||
.arg(self.connection_options.ssh_url())
|
||||
.arg("-T")
|
||||
.arg(to_run);
|
||||
.arg(self.connection_options.ssh_url());
|
||||
if !allow_pseudo_tty {
|
||||
command.arg("-T");
|
||||
}
|
||||
command.arg(to_run);
|
||||
log::debug!("ssh {:?}", command);
|
||||
command
|
||||
}
|
||||
@@ -965,8 +985,12 @@ impl SshSocket {
|
||||
shell_kind: ShellKind,
|
||||
program: &str,
|
||||
args: &[impl AsRef<str>],
|
||||
allow_pseudo_tty: bool,
|
||||
) -> Result<String> {
|
||||
let output = self.ssh_command(shell_kind, program, args).output().await?;
|
||||
let output = self
|
||||
.ssh_command(shell_kind, program, args, allow_pseudo_tty)
|
||||
.output()
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"failed to run command: {}",
|
||||
@@ -1039,7 +1063,7 @@ impl SshSocket {
|
||||
}
|
||||
|
||||
async fn platform(&self, shell: ShellKind) -> Result<RemotePlatform> {
|
||||
let uname = self.run_command(shell, "uname", &["-sm"]).await?;
|
||||
let uname = self.run_command(shell, "uname", &["-sm"], false).await?;
|
||||
let Some((os, arch)) = uname.split_once(" ") else {
|
||||
anyhow::bail!("unknown uname: {uname:?}")
|
||||
};
|
||||
@@ -1072,7 +1096,7 @@ impl SshSocket {
|
||||
async fn shell(&self) -> String {
|
||||
let default_shell = "sh";
|
||||
match self
|
||||
.run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"])
|
||||
.run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false)
|
||||
.await
|
||||
{
|
||||
Ok(shell) => match shell.trim() {
|
||||
|
||||
@@ -98,21 +98,10 @@ impl WslRemoteConnection {
|
||||
let args = &["-m"];
|
||||
let output = wsl_command_impl(options, &program, args, true)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let output = wsl_command_impl(options, &program, args, false)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Command '{}' failed: {}",
|
||||
program,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
.await;
|
||||
|
||||
if !output.is_ok_and(|output| output.status.success()) {
|
||||
run_wsl_command_impl(options, &program, args, false).await?;
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
@@ -210,8 +199,6 @@ impl WslRemoteConnection {
|
||||
return Ok(dst_path);
|
||||
}
|
||||
|
||||
delegate.set_status(Some("Installing remote server"), cx);
|
||||
|
||||
let wanted_version = match release_channel {
|
||||
ReleaseChannel::Nightly | ReleaseChannel::Dev => None,
|
||||
_ => Some(cx.update(|cx| AppVersion::global(cx))?),
|
||||
@@ -242,7 +229,7 @@ impl WslRemoteConnection {
|
||||
delegate: &Arc<dyn RemoteClientDelegate>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
delegate.set_status(Some("Uploading remote server to WSL"), cx);
|
||||
delegate.set_status(Some("Uploading remote server"), cx);
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
let parent = parent.display(PathStyle::Posix);
|
||||
|
||||
@@ -101,9 +101,7 @@ impl TableView {
|
||||
len: 0,
|
||||
font: text_font,
|
||||
color: text_style.color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
for field in table.schema.fields.iter() {
|
||||
|
||||
@@ -477,11 +477,12 @@ fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
|
||||
fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let selection = editor
|
||||
.selections
|
||||
.newest::<usize>(&editor.display_snapshot(cx));
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
buffer.language_at(selection.head()).cloned()
|
||||
let display_snapshot = editor.display_snapshot(cx);
|
||||
let selection = editor.selections.newest::<usize>(&display_snapshot);
|
||||
display_snapshot
|
||||
.buffer_snapshot()
|
||||
.language_at(selection.head())
|
||||
.cloned()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -537,9 +537,10 @@ impl TerminalElement {
|
||||
|
||||
// Private Use Area - Powerline separator symbols only
|
||||
| 0xE0B0..=0xE0B7 // Powerline separators: triangles (E0B0-E0B3) and half circles (E0B4-E0B7)
|
||||
| 0xE0B8..=0xE0BF // Additional Powerline separators: angles, flames, etc.
|
||||
| 0xE0C0..=0xE0C8 // Powerline separators: pixelated triangles, curves
|
||||
| 0xE0CC..=0xE0D4 // Powerline separators: rounded triangles, ice/lego style
|
||||
| 0xE0B8..=0xE0BF // Powerline separators: corner triangles
|
||||
| 0xE0C0..=0xE0CA // Powerline separators: flames (E0C0-E0C3), pixelated (E0C4-E0C7), and ice (E0C8 & E0CA)
|
||||
| 0xE0CC..=0xE0D1 // Powerline separators: honeycombs (E0CC-E0CD) and lego (E0CE-E0D1)
|
||||
| 0xE0D2..=0xE0D7 // Powerline separators: trapezoid (E0D2 & E0D4) and inverted triangles (E0D6-E0D7)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1112,9 +1113,7 @@ impl Element for TerminalElement {
|
||||
len,
|
||||
font: text_style.font(),
|
||||
color: theme.colors().terminal_ansi_background,
|
||||
background_color: None,
|
||||
underline: Default::default(),
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None,
|
||||
)
|
||||
@@ -1322,9 +1321,8 @@ impl Element for TerminalElement {
|
||||
len: text_to_mark.len(),
|
||||
font: ime_style.font(),
|
||||
color: ime_style.color,
|
||||
background_color: None,
|
||||
underline: ime_style.underline,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
}],
|
||||
None
|
||||
);
|
||||
@@ -1664,6 +1662,8 @@ mod tests {
|
||||
assert!(TerminalElement::is_decorative_character('\u{E0B2}')); // Powerline left triangle
|
||||
assert!(TerminalElement::is_decorative_character('\u{E0B4}')); // Powerline right half circle (the actual issue!)
|
||||
assert!(TerminalElement::is_decorative_character('\u{E0B6}')); // Powerline left half circle
|
||||
assert!(TerminalElement::is_decorative_character('\u{E0CA}')); // Powerline mirrored ice waveform
|
||||
assert!(TerminalElement::is_decorative_character('\u{E0D7}')); // Powerline left triangle inverted
|
||||
|
||||
// Characters that should NOT be considered decorative
|
||||
assert!(!TerminalElement::is_decorative_character('A')); // Regular letter
|
||||
@@ -1842,27 +1842,21 @@ mod tests {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::red(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let style2 = TextRun {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::red(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let style3 = TextRun {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::blue(), // Different color
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let font_size = AbsoluteLength::Pixels(px(12.0));
|
||||
@@ -1881,9 +1875,7 @@ mod tests {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::red(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let font_size = AbsoluteLength::Pixels(px(12.0));
|
||||
@@ -1912,9 +1904,7 @@ mod tests {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::red(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let font_size = AbsoluteLength::Pixels(px(12.0));
|
||||
@@ -1944,9 +1934,7 @@ mod tests {
|
||||
len: 1,
|
||||
font: font("Helvetica"),
|
||||
color: Hsla::red(),
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let font_size = AbsoluteLength::Pixels(px(12.0));
|
||||
|
||||
@@ -2281,12 +2281,20 @@ impl BufferSnapshot {
|
||||
} else {
|
||||
insertion_cursor.prev();
|
||||
}
|
||||
let insertion = insertion_cursor.item().expect("invalid insertion");
|
||||
let Some(insertion) = insertion_cursor.item() else {
|
||||
panic!(
|
||||
"invalid insertion for buffer {}@{:?} with anchor {:?}",
|
||||
self.remote_id(),
|
||||
self.version,
|
||||
anchor
|
||||
);
|
||||
};
|
||||
assert_eq!(
|
||||
insertion.timestamp,
|
||||
anchor.timestamp,
|
||||
"invalid insertion for buffer {} with anchor {:?}",
|
||||
"invalid insertion for buffer {}@{:?} and anchor {:?}",
|
||||
self.remote_id(),
|
||||
self.version,
|
||||
anchor
|
||||
);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -51,6 +51,9 @@ command-fds = "0.3.1"
|
||||
libc.workspace = true
|
||||
nix = { workspace = true, features = ["user"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
mach2.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tendril = "0.4.3"
|
||||
|
||||
|
||||
@@ -26,7 +26,77 @@ pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
// Create a std::process::Command first so we can use pre_exec
|
||||
let mut std_cmd = std::process::Command::new(program);
|
||||
|
||||
// WORKAROUND: Reset exception ports before exec to prevent inheritance of
|
||||
// crash handler exception ports. Due to a timing issue, child processes can
|
||||
// inherit the parent's exception ports before they're fully stabilized,
|
||||
// which can block child process spawning.
|
||||
// See: https://github.com/zed-industries/zed/issues/36754
|
||||
unsafe {
|
||||
std_cmd.pre_exec(|| {
|
||||
// Reset all exception ports to system defaults for this task.
|
||||
// This prevents the child from inheriting the parent's crash handler
|
||||
// exception ports.
|
||||
reset_exception_ports();
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to async_process::Command via From trait
|
||||
smol::process::Command::from(std_cmd)
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
||||
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||
smol::process::Command::new(program)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn reset_exception_ports() {
|
||||
use mach2::exception_types::{
|
||||
EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
|
||||
};
|
||||
use mach2::kern_return::{KERN_SUCCESS, kern_return_t};
|
||||
use mach2::mach_types::task_t;
|
||||
use mach2::port::{MACH_PORT_NULL, mach_port_t};
|
||||
use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
|
||||
use mach2::traps::mach_task_self;
|
||||
|
||||
// FFI binding for task_set_exception_ports (not exposed by mach2 crate)
|
||||
unsafe extern "C" {
|
||||
fn task_set_exception_ports(
|
||||
task: task_t,
|
||||
exception_mask: exception_mask_t,
|
||||
new_port: mach_port_t,
|
||||
behavior: exception_behavior_t,
|
||||
new_flavor: thread_state_flavor_t,
|
||||
) -> kern_return_t;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let task = mach_task_self();
|
||||
// Reset all exception ports to MACH_PORT_NULL (system default)
|
||||
// This prevents the child process from inheriting the parent's crash handler
|
||||
let kr = task_set_exception_ports(
|
||||
task,
|
||||
EXC_MASK_ALL,
|
||||
MACH_PORT_NULL,
|
||||
EXCEPTION_DEFAULT as exception_behavior_t,
|
||||
THREAD_STATE_NONE,
|
||||
);
|
||||
|
||||
if kr != KERN_SUCCESS {
|
||||
// Log but don't fail - the process can still work without this workaround
|
||||
eprintln!(
|
||||
"Warning: failed to reset exception ports in child process (kern_return: {})",
|
||||
kr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,18 +63,22 @@ impl Vim {
|
||||
}
|
||||
|
||||
fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(Operator::Literal { prefix }) = self.active_operator()
|
||||
&& let Some(prefix) = prefix
|
||||
{
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
|
||||
window.defer(cx, |window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
});
|
||||
match self.active_operator() {
|
||||
Some(Operator::Literal {
|
||||
prefix: Some(prefix),
|
||||
}) => {
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
|
||||
window.defer(cx, |window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
});
|
||||
}
|
||||
return self.handle_literal_input(prefix, "", window, cx);
|
||||
}
|
||||
return self.handle_literal_input(prefix, "", window, cx);
|
||||
Some(_) => self.insert_literal(Some(action.1), "", window, cx),
|
||||
None => log::error!(
|
||||
"Literal called when no operator was on the stack. This likely means there is an invalid keymap config"
|
||||
),
|
||||
}
|
||||
|
||||
self.insert_literal(Some(action.1), "", window, cx);
|
||||
}
|
||||
|
||||
pub fn handle_literal_keystroke(
|
||||
|
||||
@@ -1123,7 +1123,6 @@ impl Pane {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(existing_item_index) = existing_item_index {
|
||||
// If the item already exists, move it to the desired destination and activate it
|
||||
|
||||
@@ -4132,6 +4131,20 @@ impl NavHistory {
|
||||
.retain(|entry| entry.item.id() != item_id);
|
||||
}
|
||||
|
||||
pub fn rename_item(
|
||||
&mut self,
|
||||
item_id: EntityId,
|
||||
project_path: ProjectPath,
|
||||
abs_path: Option<PathBuf>,
|
||||
) {
|
||||
let mut state = self.0.lock();
|
||||
let path_for_item = state.paths_by_item.get_mut(&item_id);
|
||||
if let Some(path_for_item) = path_for_item {
|
||||
path_for_item.0 = project_path;
|
||||
path_for_item.1 = abs_path;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option<PathBuf>)> {
|
||||
self.0.lock().paths_by_item.get(&item_id).cloned()
|
||||
}
|
||||
|
||||
@@ -4325,6 +4325,10 @@ impl Workspace {
|
||||
cx.emit(Event::PaneRemoved);
|
||||
}
|
||||
|
||||
pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
|
||||
&mut self.panes
|
||||
}
|
||||
|
||||
pub fn panes(&self) -> &[Entity<Pane>] {
|
||||
&self.panes
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.212.0"
|
||||
version = "0.213.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -190,6 +190,15 @@ pub fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
unsafe {
|
||||
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
|
||||
if args.foreground {
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
}
|
||||
|
||||
// `zed --printenv` Outputs environment variables as JSON to stdout
|
||||
if args.printenv {
|
||||
util::shell_env::print_env();
|
||||
@@ -206,15 +215,6 @@ pub fn main() {
|
||||
paths::set_custom_data_dir(dir);
|
||||
}
|
||||
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
unsafe {
|
||||
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
|
||||
if args.foreground {
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
match util::get_zed_cli_path() {
|
||||
Ok(path) => askpass::set_askpass_program(path),
|
||||
|
||||
@@ -1922,6 +1922,7 @@ fn open_bundled_file(
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_should_serialize(false, cx);
|
||||
editor.set_breadcrumb_header(title.into());
|
||||
editor
|
||||
})),
|
||||
|
||||
@@ -12,7 +12,7 @@ use collections::HashSet;
|
||||
use gpui::AsyncApp;
|
||||
|
||||
use crate::{
|
||||
example::{Example, NamedExample},
|
||||
example::{Example, Excerpt, NamedExample},
|
||||
headless::ZetaCliAppState,
|
||||
predict::{PredictionDetails, zeta2_predict},
|
||||
};
|
||||
@@ -39,6 +39,11 @@ pub async fn run_evaluate(
|
||||
let aggregated_result = EvaluationResult {
|
||||
context: Scores::aggregate(all_results.iter().map(|r| &r.context)),
|
||||
edit_prediction: Scores::aggregate(all_results.iter().map(|r| &r.edit_prediction)),
|
||||
edit_sites_coverage: all_results
|
||||
.iter()
|
||||
.map(|r| r.edit_sites_coverage)
|
||||
.sum::<f64>()
|
||||
/ all_results.len() as f64,
|
||||
};
|
||||
|
||||
if example_len > 1 {
|
||||
@@ -106,6 +111,12 @@ pub async fn run_evaluate_one(
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EvaluationResult {
|
||||
pub context: Scores,
|
||||
|
||||
/// Ratio of edited lines that we expect to edit (as indicated in the
|
||||
/// expected patch) AND were included into the context
|
||||
/// num_correctly_retrieved_lines / num_expected_lines
|
||||
pub edit_sites_coverage: f64,
|
||||
|
||||
pub edit_prediction: Scores,
|
||||
}
|
||||
|
||||
@@ -123,12 +134,12 @@ impl Scores {
|
||||
pub fn to_markdown(&self) -> String {
|
||||
format!(
|
||||
"
|
||||
Precision : {:.4}
|
||||
Recall : {:.4}
|
||||
F1 Score : {:.4}
|
||||
True Positives : {}
|
||||
False Positives : {}
|
||||
False Negatives : {}",
|
||||
Precision : {:.4}
|
||||
Recall : {:.4}
|
||||
F1 Score : {:.4}
|
||||
True Positives : {}
|
||||
False Positives : {}
|
||||
False Negatives : {}",
|
||||
self.precision,
|
||||
self.recall,
|
||||
self.f1_score,
|
||||
@@ -169,17 +180,25 @@ impl Scores {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EditSitesScores {
|
||||
num_edit_sites: u32,
|
||||
num_correctly_retrieved: u32,
|
||||
}
|
||||
|
||||
impl EvaluationResult {
|
||||
pub fn to_markdown(&self) -> String {
|
||||
format!(
|
||||
r#"
|
||||
### Context Scores
|
||||
{}
|
||||
Edit sites coverage: {}
|
||||
|
||||
### Edit Prediction Scores
|
||||
{}
|
||||
"#,
|
||||
self.context.to_markdown(),
|
||||
self.edit_sites_coverage,
|
||||
self.edit_prediction.to_markdown()
|
||||
)
|
||||
}
|
||||
@@ -229,9 +248,54 @@ pub fn evaluate(example: &Example, preds: &PredictionDetails) -> EvaluationResul
|
||||
|
||||
result.edit_prediction = precision_recall(&expected_patch_lines, &actual_patch_lines);
|
||||
|
||||
result.edit_sites_coverage =
|
||||
calculate_edit_sites_coverage(&example.expected_patch, &preds.excerpts);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Compute the ratio of lines that we expect to edit (are in the expected patch) that
|
||||
/// were included in the retrieved context
|
||||
/// `num_correctly_retrieved_lines / num_edited_lines_in_expected_patch`
|
||||
///
|
||||
/// In order to make an edit in some line, the model has to have an access to this line.
|
||||
/// If we don't include the line in the retrieved context, there's no chance to make an edit.
|
||||
///
|
||||
/// This metric reflects that, where 1.0 -- we retrieved all lines to be
|
||||
/// edited, and 0.0 -- we retrieved none of them.
|
||||
///
|
||||
/// Example:
|
||||
fn calculate_edit_sites_coverage(patch: &str, excerpts: &[Excerpt]) -> EditSitesScores {
|
||||
// todo:
|
||||
let expected_patch_lines = patch
|
||||
.lines()
|
||||
.map(DiffLine::parse)
|
||||
.filter_map(|line| match line {
|
||||
DiffLine::Deletion(text) => Some(text.trim().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let correct_cases = expected_patch_lines
|
||||
.iter()
|
||||
.filter(|line| {
|
||||
excerpts.iter().any(|excerpt| {
|
||||
excerpt
|
||||
.text
|
||||
.lines()
|
||||
.any(|excerpt_line| excerpt_line == *line)
|
||||
})
|
||||
})
|
||||
.count();
|
||||
let total_cases = expected_patch_lines.len();
|
||||
|
||||
if total_cases == 0 {
|
||||
0.0
|
||||
} else {
|
||||
correct_cases as f64 / total_cases as f64
|
||||
}
|
||||
}
|
||||
|
||||
fn precision_recall(expected: &HashSet<String>, actual: &HashSet<String>) -> Scores {
|
||||
let true_positives = expected.intersection(actual).count();
|
||||
let false_positives = actual.difference(expected).count();
|
||||
@@ -336,3 +400,59 @@ pub fn compare_diffs(patch_a: &str, patch_b: &str) -> String {
|
||||
|
||||
annotated.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::calculate_edit_sites_coverage;
|
||||
use crate::example::Excerpt;
|
||||
|
||||
#[test]
|
||||
fn test_evaluate_expected_edit_places() {
|
||||
let patch = indoc::indoc! {"
|
||||
--- a/test.txt
|
||||
+++ b/test.txt
|
||||
@@ -1,4 +1,4 @@
|
||||
apple
|
||||
-banana
|
||||
+BANANA
|
||||
cherry
|
||||
-date
|
||||
+DATE
|
||||
"};
|
||||
|
||||
let one_correct_excerpt = vec![Excerpt {
|
||||
path: "test.txt".into(),
|
||||
text: "apple\nbanana\n".to_string(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
calculate_edit_sites_coverage(&patch, &one_correct_excerpt),
|
||||
0.5,
|
||||
);
|
||||
|
||||
let both_correct_excerpts = vec![
|
||||
Excerpt {
|
||||
path: "test.txt".into(),
|
||||
text: "apple\nbanana\n".to_string(),
|
||||
},
|
||||
Excerpt {
|
||||
path: "test.txt".into(),
|
||||
text: "cherry\ndate\n".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
calculate_edit_sites_coverage(&patch, &both_correct_excerpts),
|
||||
1.0,
|
||||
);
|
||||
|
||||
let incorrect_excerpts = vec![Excerpt {
|
||||
path: "test.txt".into(),
|
||||
text: "apple\n".into(),
|
||||
}];
|
||||
assert_eq!(
|
||||
calculate_edit_sites_coverage(&patch, &incorrect_excerpts),
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +234,20 @@ impl NamedExample {
|
||||
}
|
||||
|
||||
// Resolve the example to a revision, fetching it if needed.
|
||||
let revision = run_git(&repo_dir, &["rev-parse", &self.example.revision]).await;
|
||||
let revision = run_git(
|
||||
&repo_dir,
|
||||
&[
|
||||
"rev-parse",
|
||||
&format!("{}^{{commit}}", &self.example.revision),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let revision = if let Ok(revision) = revision {
|
||||
revision
|
||||
} else {
|
||||
run_git(
|
||||
&repo_dir,
|
||||
&["fetch", "--depth", "1", "origin", &self.example.revision],
|
||||
&["fetch", "--depth", "2", "origin", &self.example.revision],
|
||||
)
|
||||
.await?;
|
||||
let revision = run_git(&repo_dir, &["rev-parse", "FETCH_HEAD"]).await?;
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
- [Theme Extensions](./extensions/themes.md)
|
||||
- [Icon Theme Extensions](./extensions/icon-themes.md)
|
||||
- [Slash Command Extensions](./extensions/slash-commands.md)
|
||||
- [Agent Server Extensions](./extensions/agent-servers.md)
|
||||
- [MCP Server Extensions](./extensions/mcp-extensions.md)
|
||||
|
||||
# Language Support
|
||||
|
||||
@@ -11,7 +11,7 @@ You can do that by:
|
||||
|
||||
1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models
|
||||
2. [using your own API keys](./llm-providers.md#use-your-own-keys), either from model providers like Anthropic or model gateways like OpenRouter.
|
||||
3. using an external agent like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code)
|
||||
3. using an [external agent](./external-agents.md) like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code)
|
||||
|
||||
## Overview {#overview}
|
||||
|
||||
@@ -21,7 +21,7 @@ If you need extra room to type, you can expand the message editor with {#kb agen
|
||||
You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt.
|
||||
From this point on, you can interact with the many supported features outlined below.
|
||||
|
||||
> Note that for external agents, like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code), some of the features outlined below are _not_ currently supported—for example, _restoring threads from history_, _checkpoints_, _token usage display_, _model selection_, and others. All of them should hopefully be supported in the future.
|
||||
> Note that for external agents, like [Gemini CLI](./external-agents.md#gemini-cli) or [Claude Code](./external-agents.md#claude-code), some of the features outlined below may _not_ be supported—for example, _restoring threads from history_, _checkpoints_, _token usage display_, and others. Their availability varies depending on the agent.
|
||||
|
||||
### Creating New Threads {#new-thread}
|
||||
|
||||
@@ -83,11 +83,13 @@ Although Zed's agent is very efficient at reading through your code base to auto
|
||||
In Zed's Agent Panel, all pieces of context are added as mentions in the panel's message editor.
|
||||
You can type `@` to mention files, directories, symbols, previous threads, and rules files.
|
||||
|
||||
Additionally, you can also select text in a buffer and add it as context by using the {#kb agent::AddSelectionToThread} keybinding, running the {#action agent::AddSelectionToThread} action, or choosing the "Selection" item in the `@` menu.
|
||||
|
||||
Copying images and pasting them in the panel's message editor is also supported.
|
||||
|
||||
### Token Usage {#token-usage}
|
||||
### Selection as Context
|
||||
|
||||
Additionally, you can also select text in a buffer and add it as context by using the {#kb agent::AddSelectionToThread} keybinding, running the {#action agent::AddSelectionToThread} action, or choosing the "Selection" item in the `@` menu.
|
||||
|
||||
## Token Usage {#token-usage}
|
||||
|
||||
Zed surfaces how many tokens you are consuming for your currently active thread near the profile selector in the panel's message editor. Depending on how many pieces of context you add, your token consumption can grow rapidly.
|
||||
|
||||
@@ -98,7 +100,8 @@ You can also do this at any time with an ongoing thread via the "Agent Options"
|
||||
|
||||
After you've configured your LLM providers—either via [a custom API key](./llm-providers.md) or through [Zed's hosted models](./models.md)—you can switch between them by clicking on the model selector on the message editor or by using the {#kb agent::ToggleModelSelector} keybinding.
|
||||
|
||||
> The same model can be offered via multiple providers - for example, Claude Sonnet 4 is available via Zed Pro, OpenRouter, Anthropic directly, and more. Make sure you've selected the correct model **_provider_** for the model you'd like to use, delineated by the logo to the left of the model in the model selector.
|
||||
> The same model can be offered via multiple providers - for example, Claude Sonnet 4 is available via Zed Pro, OpenRouter, Anthropic directly, and more.
|
||||
> Make sure you've selected the correct model **_provider_** for the model you'd like to use, delineated by the logo to the left of the model in the model selector.
|
||||
|
||||
## Using Tools {#using-tools}
|
||||
|
||||
@@ -118,19 +121,21 @@ Zed offers three built-in profiles and you can create as many custom ones as you
|
||||
- `Ask`: A profile with read-only tools. Best for asking questions about your code base without the concern of the agent making changes.
|
||||
- `Minimal`: A profile with no tools. Best for general conversations with the LLM where no knowledge of your code base is necessary.
|
||||
|
||||
You can explore the exact tools enabled in each profile by clicking on the profile selector button > `Configure Profiles…` > the one you want to check out.
|
||||
You can explore the exact tools enabled in each profile by clicking on the profile selector button > `Configure` button > the one you want to check out.
|
||||
|
||||
Alternatively, you can also use either the command palette, by running {#action agent::ManageProfiles}, or the keybinding directly, {#kb agent::ManageProfiles}, to have access to the profile management modal.
|
||||
|
||||
#### Custom Profiles {#custom-profiles}
|
||||
|
||||
You can create a custom profile via the `Configure Profiles…` option in the profile selector.
|
||||
From here, you can choose to `Add New Profile` or fork an existing one with a custom name and your preferred set of tools.
|
||||
You can also create a custom profile through the Agent Profile modal.
|
||||
From there, you can choose to `Add New Profile` or fork an existing one with a custom name and your preferred set of tools.
|
||||
|
||||
You can also override built-in profiles.
|
||||
With a built-in profile selected, in the profile selector, navigate to `Configure Tools`, and select the tools you'd like.
|
||||
It's also possible to override built-in profiles.
|
||||
In the Agent Profile modal, select a built-in profile, navigate to `Configure Tools`, and rearrange the tools you'd like to keep or remove.
|
||||
|
||||
Zed will store this profile in your settings using the same profile name as the default you overrode.
|
||||
|
||||
All custom profiles can be edited via the UI or by hand under the `assistant.profiles` key in your `settings.json` file.
|
||||
All custom profiles can be edited via the UI or by hand under the `agent.profiles` key in your `settings.json` file.
|
||||
|
||||
### Tool Approval
|
||||
|
||||
@@ -141,19 +146,24 @@ You can change that by setting this key to `true` in either your `settings.json`
|
||||
### Model Support {#model-support}
|
||||
|
||||
Tool calling needs to be individually supported by each model and model provider.
|
||||
Therefore, despite the presence of tools, some models may not have the ability to pick them up yet in Zed. You should see a "No tools" label if you select a model that falls into this case.
|
||||
Therefore, despite the presence of tools, some models may not have the ability to pick them up yet in Zed.
|
||||
You should see a "No tools" label if you select a model that falls into this case.
|
||||
|
||||
All [Zed's hosted models](./models.md) support tool calling out-of-the-box.
|
||||
|
||||
### MCP Servers {#mcp-servers}
|
||||
|
||||
Similarly to the built-in tools, some models may not support all tools included in a given MCP Server. Zed's UI will inform you about this via a warning icon that appears close to the model selector.
|
||||
Similarly to the built-in tools, some models may not support all tools included in a given MCP Server.
|
||||
Zed's UI will inform you about this via a warning icon that appears close to the model selector.
|
||||
|
||||
## Text Threads {#text-threads}
|
||||
|
||||
["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text. With text threads, you have full control over the conversation data. You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation.
|
||||
["Text Threads"](./text-threads.md) present your conversation with the LLM in a different format—as raw text.
|
||||
With text threads, you have full control over the conversation data.
|
||||
You can remove and edit responses from the LLM, swap roles, and include more context earlier in the conversation.
|
||||
|
||||
For users who have been with us for some time, you'll notice that text threads are our original assistant panel—users love it for the control it offers. We do not plan to deprecate text threads, but it should be noted that if you want the AI to write to your code base autonomously, that's only available in the newer, and now default, "Threads".
|
||||
For users who have been with us for some time, you'll notice that text threads are our original assistant panel—users love it for the control it offers.
|
||||
We do not plan to deprecate text threads, but it should be noted that if you want the AI to write to your code base autonomously, that's only available in the newer, and now default, "Threads".
|
||||
|
||||
## Errors and Debugging {#errors-and-debugging}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Zed supports terminal-based agents through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com).
|
||||
|
||||
Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation.
|
||||
[Claude Code](https://www.anthropic.com/claude-code) and [Codex](https://developers.openai.com/codex) are also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well.
|
||||
[Claude Code](https://www.anthropic.com/claude-code) and [Codex](https://developers.openai.com/codex) are also included by default, and you can [add custom ACP-compatible agents](#add-more-agents) as well.
|
||||
|
||||
> Note that Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider.
|
||||
> Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models.
|
||||
@@ -182,14 +182,18 @@ And to give it context, you can @-mention files, symbols, or fetch the web.
|
||||
> Note that some first-party agent features don't yet work with Codex: editing past messages, resuming threads from history, and checkpointing.
|
||||
> We hope to add these features in the near future.
|
||||
|
||||
## Add Custom Agents {#add-custom-agents}
|
||||
## Add More Agents {#add-more-agents}
|
||||
|
||||
You can run any agent speaking ACP in Zed by changing your settings as follows:
|
||||
Add more external agents to Zed by installing [Agent Server extensions](../extensions/agent-servers.md).
|
||||
|
||||
See what agents are available by filtering for "Agent Servers" in the extensions page, which you can access via the command palette with `zed: extensions`, or the [Zed website](https://zed.dev/extensions?filter=agent-servers).
|
||||
|
||||
You can also add agents through your `settings.json`, by specifying certain fields under `agent_servers`, like so:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"agent_servers": {
|
||||
"Custom Agent": {
|
||||
"My Custom Agent": {
|
||||
"command": "node",
|
||||
"args": ["~/projects/agent/index.js", "--acp"],
|
||||
"env": {}
|
||||
@@ -198,9 +202,9 @@ You can run any agent speaking ACP in Zed by changing your settings as follows:
|
||||
}
|
||||
```
|
||||
|
||||
This can also be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it.
|
||||
This can be useful if you're in the middle of developing a new agent that speaks the protocol and you want to debug it.
|
||||
|
||||
You can also specify a custom path, arguments, or environment for the builtin integrations by using the `claude` and `gemini` names.
|
||||
It's also possible to specify a custom path, arguments, or environment for the builtin integrations by using the `claude` and `gemini` names.
|
||||
|
||||
## Debugging Agents
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Using Rules {#using-rules}
|
||||
|
||||
A rule is essentially a prompt that is inserted at the beginning of each interaction with the Agent.
|
||||
Currently, Zed supports `.rules` files at the directory's root and the Rules Library, which allows you to store multiple rules for on-demand usage.
|
||||
Currently, Zed supports adding rules through files inserted directly in the worktree or through the Rules Library, which allows you to store multiple rules for constant or on-demand usage.
|
||||
|
||||
## `.rules` files
|
||||
|
||||
@@ -30,7 +30,7 @@ You can use the inline assistant right in the rules editor, allowing you to auto
|
||||
2. Click on the Agent menu (`...`) in the top right corner.
|
||||
3. Select `Rules...` from the dropdown.
|
||||
|
||||
You can also use the `agent: open rules library` command while in the Agent Panel.
|
||||
You can also reach it by running {#action agent::OpenRulesLibrary} in the command palette or through the {#kb agent::OpenRulesLibrary} keybinding.
|
||||
|
||||
### Managing Rules
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ Zed lets you add new functionality using user-defined extensions.
|
||||
- [Developing Themes](./extensions/themes.md)
|
||||
- [Developing Icon Themes](./extensions/icon-themes.md)
|
||||
- [Developing Slash Commands](./extensions/slash-commands.md)
|
||||
- [Developing Agent Servers](./extensions/agent-servers.md)
|
||||
- [Developing MCP Servers](./extensions/mcp-extensions.md)
|
||||
|
||||
160
docs/src/extensions/agent-servers.md
Normal file
160
docs/src/extensions/agent-servers.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Agent Server Extensions
|
||||
|
||||
Agent Servers are programs that provide AI agent implementations through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com).
|
||||
Agent Server Extensions let you package up an Agent Server so that users can install the extension and have your agent easily available to use in Zed.
|
||||
|
||||
You can see the current Agent Server extensions either by opening the Extensions tab in Zed (execute the `zed: extensions` command) and changing the filter from `All` to `Agent Servers`, or by visiting [the Zed website](https://zed.dev/extensions?filter=agent-servers).
|
||||
|
||||
## Defining Agent Server Extensions
|
||||
|
||||
An extension can register one or more agent servers in the `extension.toml` like so:
|
||||
|
||||
```toml
|
||||
[agent_servers.my-agent]
|
||||
name = "My Agent"
|
||||
|
||||
[agent_servers.my-agent.targets.darwin-aarch64]
|
||||
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.tar.gz"
|
||||
cmd = "./agent"
|
||||
args = ["--serve"]
|
||||
|
||||
[agent_servers.my-agent.targets.linux-x86_64]
|
||||
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-linux-x64.tar.gz"
|
||||
cmd = "./agent"
|
||||
args = ["--serve"]
|
||||
|
||||
[agent_servers.my-agent.targets.windows-x86_64]
|
||||
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-windows-x64.zip"
|
||||
cmd = "./agent.exe"
|
||||
args = ["--serve"]
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
- `name`: A human-readable display name for the agent server (shown in menus)
|
||||
- `targets`: Platform-specific configurations for downloading and running the agent
|
||||
|
||||
### Target Configuration
|
||||
|
||||
Each target key uses the format `{os}-{arch}` where:
|
||||
|
||||
- **os**: `darwin` (macOS), `linux`, or `windows`
|
||||
- **arch**: `aarch64` (ARM64) or `x86_64`
|
||||
|
||||
Each target must specify:
|
||||
|
||||
- `archive`: URL to download the archive from (supports `.tar.gz`, `.zip`, etc.)
|
||||
- `cmd`: Command to run the agent server (relative to the extracted archive)
|
||||
- `args`: Command-line arguments to pass to the agent server (optional)
|
||||
|
||||
### Optional Fields
|
||||
|
||||
You can also optionally specify:
|
||||
|
||||
- `sha256`: SHA-256 hash string of the archive's bytes. Zed will check this after the archive is downloaded and give an error if it doesn't match, so doing this improves security.
|
||||
- `env`: Environment variables to set in the agent's spawned process.
|
||||
- `icon`: Path to an SVG icon (relative to extension root) for display in menus.
|
||||
|
||||
### Complete Example
|
||||
|
||||
Here's a more complete example with all optional fields:
|
||||
|
||||
```toml
|
||||
[agent_servers.example-agent]
|
||||
name = "Example Agent"
|
||||
icon = "icon/agent.svg"
|
||||
|
||||
[agent_servers.example-agent.env]
|
||||
AGENT_LOG_LEVEL = "info"
|
||||
AGENT_MODE = "production"
|
||||
|
||||
[agent_servers.example-agent.targets.darwin-aarch64]
|
||||
archive = "https://github.com/example/agent/releases/download/v2.0.0/agent-darwin-arm64.tar.gz"
|
||||
cmd = "./bin/agent"
|
||||
args = ["serve", "--port", "8080"]
|
||||
sha256 = "abc123def456..."
|
||||
|
||||
[agent_servers.example-agent.targets.linux-x86_64]
|
||||
archive = "https://github.com/example/agent/releases/download/v2.0.0/agent-linux-x64.tar.gz"
|
||||
cmd = "./bin/agent"
|
||||
args = ["serve", "--port", "8080"]
|
||||
sha256 = "def456abc123..."
|
||||
```
|
||||
|
||||
## Installation Process
|
||||
|
||||
When a user installs your extension and selects the agent server:
|
||||
|
||||
1. Zed downloads the appropriate archive for the user's platform
|
||||
2. The archive is extracted to a cache directory
|
||||
3. Zed launches the agent using the specified command and arguments
|
||||
4. Environment variables are set as configured
|
||||
5. The agent server runs in the background, ready to assist the user
|
||||
|
||||
Archives are cached locally, so subsequent launches are fast.
|
||||
|
||||
## Distribution Best Practices
|
||||
|
||||
### Use GitHub Releases
|
||||
|
||||
GitHub Releases are a reliable way to distribute agent server binaries:
|
||||
|
||||
1. Build your agent for each platform (macOS ARM64, macOS x86_64, Linux x86_64, Windows x86_64)
|
||||
2. Package each build as a compressed archive (`.tar.gz` or `.zip`)
|
||||
3. Create a GitHub release and upload the archives
|
||||
4. Use the release URLs in your `extension.toml`
|
||||
|
||||
## SHA-256 Hashes
|
||||
|
||||
It's good for security to include SHA-256 hashes of your archives in `extension.toml`. Here's how to generate it:
|
||||
|
||||
### macOS and Linux
|
||||
|
||||
```bash
|
||||
shasum -a 256 agent-darwin-arm64.tar.gz
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
certutil -hashfile agent-windows-x64.zip SHA256
|
||||
```
|
||||
|
||||
Then add that string to your target configuration:
|
||||
|
||||
```toml
|
||||
[agent_servers.my-agent.targets.darwin-aarch64]
|
||||
archive = "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.tar.gz"
|
||||
cmd = "./agent"
|
||||
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
To test your Agent Server Extension:
|
||||
|
||||
1. [Install it as a dev extension](./developing-extensions.md#developing-an-extension-locally)
|
||||
2. Open the [Agent Panel](../ai/agent-panel.md)
|
||||
3. Select your Agent Server from the list
|
||||
4. Verify that it downloads, installs, and launches correctly
|
||||
5. Test its functionality by conversing with it and watching the [ACP logs](../ai/external-agents.md#debugging-agents)
|
||||
|
||||
## Icon Guideline
|
||||
|
||||
In case your agent server has a logo, we highly recommend adding it as an SVG icon.
|
||||
For optimal display, follow these guidelines:
|
||||
|
||||
- Make sure you resize your SVG to fit a 16x16 bounding box, with a padding of around one or two pixels
|
||||
- Ensure you have a clean SVG code by processing it through [SVGOMG](https://jakearchibald.github.io/svgomg/)
|
||||
- Avoid including icons with gradients as they will often make the SVG more complicated and possibly not render perfectly
|
||||
|
||||
Note that we'll automatically convert your icon to monochrome to preserve Zed's design consistency.
|
||||
(You can still use opacity in different paths of your SVG to add visual layering.)
|
||||
|
||||
---
|
||||
|
||||
This is all you need to distribute an agent server through Zed's extension system!
|
||||
|
||||
## Publishing
|
||||
|
||||
Once your extension is ready, see [Publishing your extension](./developing-extensions.md#publishing-your-extension) to learn how to submit it to the Zed extension registry.
|
||||
@@ -75,30 +75,30 @@ Zed supports machines with Intel (x86_64) or Apple (aarch64) processors that mee
|
||||
|
||||
### Linux
|
||||
|
||||
Zed supports 64bit Intel/AMD (x86_64) and 64Bit ARM (aarch64) processors.
|
||||
Zed supports 64-bit Intel/AMD (x86_64) and 64-bit Arm (aarch64) processors.
|
||||
|
||||
Zed requires a Vulkan 1.3 driver, and the following desktop portals:
|
||||
Zed requires a Vulkan 1.3 driver and the following desktop portals:
|
||||
|
||||
- `org.freedesktop.portal.FileChooser`
|
||||
- `org.freedesktop.portal.OpenURI`
|
||||
- `org.freedesktop.portal.Secret`, or `org.freedesktop.Secrets`
|
||||
- `org.freedesktop.portal.Secret` or `org.freedesktop.Secrets`
|
||||
|
||||
### Windows
|
||||
|
||||
Zed supports the follow Windows releases:
|
||||
| Version | Microsoft Status | Zed Status |
|
||||
| ------------------------- | ------------------ | ------------------- |
|
||||
| Windows 11 (all releases) | Supported | Supported |
|
||||
| Windows 10 (64-bit) | Supported | Supported |
|
||||
Zed supports the following Windows releases:
|
||||
| Version | Zed Status |
|
||||
| ------------------------- | ------------------- |
|
||||
| Windows 11, version 22H2 and later | Supported |
|
||||
| Windows 10, version 1903 and later | Supported |
|
||||
|
||||
A 64-bit operating system is required to run Zed.
|
||||
|
||||
#### Windows Hardware
|
||||
|
||||
Zed supports machines with Intel or AMD 64-bit (x86_64) processors that meet the above Windows requirements:
|
||||
Zed supports machines with x64 (Intel, AMD) or Arm64 (Qualcomm) processors that meet the following requirements:
|
||||
|
||||
- Windows 11 (64-bit)
|
||||
- Windows 10 (64-bit)
|
||||
- Graphics: A GPU that supports DirectX 11 (most PCs from 2012+).
|
||||
- Driver: Current NVIDIA/AMD/Intel driver (not the Microsoft Basic Display Adapter).
|
||||
- Driver: Current NVIDIA/AMD/Intel/Qualcomm driver (not the Microsoft Basic Display Adapter).
|
||||
|
||||
### FreeBSD
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ See [Configuring Zed](./configuring-zed.md) for additional information and other
|
||||
|
||||
You can install many [themes](./themes.md) and [icon themes](./icon-themes.md) in form of extensions by running {#action zed::Extensions} from the command palette.
|
||||
|
||||
You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and {#action icon_theme_selector::Toggle} ({#kb icon_theme_selector::Toggle}) which will modify the following settings:
|
||||
You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and {#action icon_theme_selector::Toggle} which will modify the following settings:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
||||
@@ -23,7 +23,8 @@ For detailed instructions on setting up and using remote development features, i
|
||||
|
||||
### Zed fails to start or shows a blank window
|
||||
|
||||
- Update your GPU drivers from your GPU vendor (Intel/AMD/NVIDIA).
|
||||
- Check that your hardware and operating system version are compatible with Zed. See our [installation guide](./installation.md) for more information.
|
||||
- Update your GPU drivers from your GPU vendor (Intel/AMD/NVIDIA/Qualcomm).
|
||||
- Ensure hardware acceleration is enabled in Windows and not blocked by third‑party software.
|
||||
- Try launching Zed with no extensions or custom settings to isolate conflicts.
|
||||
|
||||
@@ -39,14 +40,14 @@ When prompted for credentials, use the graphical askpass dialog. If it doesn’t
|
||||
|
||||
#### Zed fails to open / degraded performance
|
||||
|
||||
Zed requires a DX11 compatible GPU to run, if Zed doesn't open for you it is possible that your GPU does not meet the minimum requirements.
|
||||
Zed requires a DirectX 11 compatible GPU to run. If Zed fails to open, your GPU may not meet the minimum requirements.
|
||||
|
||||
To check if your GPU supports DX11, you can use the following command:
|
||||
To check if your GPU supports DirectX 11, run the following command:
|
||||
|
||||
```
|
||||
dxdiag
|
||||
```
|
||||
|
||||
Which will open the diagnostic tool that will show the minimum DirectX version your GPU supports under `System` → `System Information` → `DirectX Version`.
|
||||
This will open the DirectX Diagnostic Tool, which shows the DirectX version your GPU supports under `System` → `System Information` → `DirectX Version`.
|
||||
|
||||
You might also be trying to run Zed inside a virtual machine in which case it will use the emulated adapter that your VM provides, while Zed will work the performance will be degraded.
|
||||
If you're running Zed inside a virtual machine, it will use the emulated adapter provided by your VM. While Zed will work in this environment, performance may be degraded.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))?;
|
||||
|
||||
123
tooling/xtask/src/tasks/workflows/after_release.rs
Normal file
123
tooling/xtask/src/tasks/workflows/after_release.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use gh_workflow::*;
|
||||
|
||||
use crate::tasks::workflows::{
|
||||
runners,
|
||||
steps::{NamedJob, dependant_job, named},
|
||||
vars::{self, StepOutput},
|
||||
};
|
||||
|
||||
pub fn after_release() -> Workflow {
|
||||
let refresh_zed_dev = rebuild_releases_page();
|
||||
let post_to_discord = post_to_discord(&[&refresh_zed_dev]);
|
||||
let publish_winget = publish_winget();
|
||||
|
||||
named::workflow()
|
||||
.on(Event::default().release(Release::default().types(vec![ReleaseType::Published])))
|
||||
.add_job(refresh_zed_dev.name, refresh_zed_dev.job)
|
||||
.add_job(post_to_discord.name, post_to_discord.job)
|
||||
.add_job(publish_winget.name, publish_winget.job)
|
||||
}
|
||||
|
||||
fn rebuild_releases_page() -> NamedJob {
|
||||
named::job(
|
||||
Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(
|
||||
"github.repository_owner == 'zed-industries'",
|
||||
))
|
||||
.add_step(named::bash(
|
||||
"curl https://zed.dev/api/revalidate-releases -H \"Authorization: Bearer ${RELEASE_NOTES_API_TOKEN}\"",
|
||||
).add_env(("RELEASE_NOTES_API_TOKEN", vars::RELEASE_NOTES_API_TOKEN))),
|
||||
)
|
||||
}
|
||||
|
||||
fn post_to_discord(deps: &[&NamedJob]) -> NamedJob {
|
||||
fn get_release_url() -> Step<Run> {
|
||||
named::bash(indoc::indoc! {r#"
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
fi
|
||||
|
||||
echo "URL=$URL" >> "$GITHUB_OUTPUT"
|
||||
"#})
|
||||
.id("get-release-url")
|
||||
}
|
||||
|
||||
fn get_content() -> Step<Use> {
|
||||
named::uses(
|
||||
"2428392",
|
||||
"gh-truncate-string-action",
|
||||
"b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1
|
||||
)
|
||||
.id("get-content")
|
||||
.add_with((
|
||||
"stringToTruncate",
|
||||
indoc::indoc! {r#"
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
"#},
|
||||
))
|
||||
.add_with(("maxLength", 2000))
|
||||
.add_with(("truncationSymbol", "..."))
|
||||
}
|
||||
|
||||
fn discord_webhook_action() -> Step<Use> {
|
||||
named::uses(
|
||||
"tsickert",
|
||||
"discord-webhook",
|
||||
"c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0
|
||||
)
|
||||
.add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES))
|
||||
.add_with(("content", "${{ steps.get-content.outputs.string }}"))
|
||||
}
|
||||
let job = dependant_job(deps)
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(
|
||||
"github.repository_owner == 'zed-industries'",
|
||||
))
|
||||
.add_step(get_release_url())
|
||||
.add_step(get_content())
|
||||
.add_step(discord_webhook_action());
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
fn publish_winget() -> NamedJob {
|
||||
fn set_package_name() -> (Step<Run>, StepOutput) {
|
||||
let step = named::bash(indoc::indoc! {r#"
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
"#})
|
||||
.id("set-package-name");
|
||||
|
||||
let output = StepOutput::new(&step, "PACKAGE_NAME");
|
||||
(step, output)
|
||||
}
|
||||
|
||||
fn winget_releaser(package_name: &StepOutput) -> Step<Use> {
|
||||
named::uses(
|
||||
"vedantmgoyal9",
|
||||
"winget-releaser",
|
||||
"19e706d4c9121098010096f9c495a70a7518b30f", // v2
|
||||
)
|
||||
.add_with(("identifier", package_name.to_string()))
|
||||
.add_with(("max-versions-to-keep", 5))
|
||||
.add_with(("token", vars::WINGET_TOKEN))
|
||||
}
|
||||
|
||||
let (set_package_name, package_name) = set_package_name();
|
||||
|
||||
named::job(
|
||||
Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.add_step(set_package_name)
|
||||
.add_step(winget_releaser(&package_name)),
|
||||
)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
&should_run_tests,
|
||||
]);
|
||||
|
||||
let jobs = [
|
||||
let mut jobs = vec![
|
||||
orchestrate,
|
||||
check_style(),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Windows)),
|
||||
@@ -50,7 +50,6 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
||||
should_run_tests.guard(doctests()),
|
||||
should_run_tests.guard(check_workspace_binaries()),
|
||||
should_run_tests.guard(check_postgres_and_protobuf_migrations()), // could be more specific here?
|
||||
should_run_tests.guard(check_dependencies()), // could be more specific here?
|
||||
should_check_docs.guard(check_docs()),
|
||||
should_check_licences.guard(check_licenses()),
|
||||
@@ -74,6 +73,8 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
];
|
||||
let tests_pass = tests_pass(&jobs);
|
||||
|
||||
jobs.push(should_run_tests.guard(check_postgres_and_protobuf_migrations())); // could be more specific here?
|
||||
|
||||
named::workflow()
|
||||
.add_event(Event::default()
|
||||
.push(
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user