Compare commits
40 Commits
git-clone
...
git-integr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa901e70e | ||
|
|
334ca21857 | ||
|
|
f58278aaf4 | ||
|
|
e10b9b70ef | ||
|
|
098adf3bdd | ||
|
|
a85c508f69 | ||
|
|
2a713c546b | ||
|
|
f937c1931f | ||
|
|
7a62f01ea5 | ||
|
|
2d071b0cb6 | ||
|
|
bd2b0de231 | ||
|
|
886de8f54b | ||
|
|
7a783a91cc | ||
|
|
f9462da2f7 | ||
|
|
61dd6a8f31 | ||
|
|
abb199c85e | ||
|
|
cebbf77491 | ||
|
|
0180f3e72a | ||
|
|
5488a19221 | ||
|
|
bb1198e7d6 | ||
|
|
69fe27f45e | ||
|
|
469da2fd07 | ||
|
|
4f87822133 | ||
|
|
9a69d89f88 | ||
|
|
54f360ace1 | ||
|
|
b2a0b78ece | ||
|
|
f1ca2f9f31 | ||
|
|
4b34adedd2 | ||
|
|
df48294caa | ||
|
|
cdc5cc348f | ||
|
|
0f7f540138 | ||
|
|
184001b33b | ||
|
|
225a2a8a20 | ||
|
|
ea37057814 | ||
|
|
77cdef3596 | ||
|
|
05108c50fd | ||
|
|
07538ff08e | ||
|
|
9073a2666c | ||
|
|
843a35a1a9 | ||
|
|
aff93f2f6c |
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,7 +61,8 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: extension_tests::run_clippy
|
||||
|
||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -71,15 +72,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -93,6 +94,8 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -111,7 +114,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,7 +20,8 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -44,7 +45,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
55
.github/workflows/run_tests.yml
vendored
55
.github/workflows/run_tests.yml
vendored
@@ -74,18 +74,19 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: ./script/prettier
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
|
||||
- id: record_style_failure
|
||||
name: steps::record_style_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -96,6 +97,8 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
outputs:
|
||||
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -116,7 +119,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -163,15 +167,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -185,6 +189,8 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_mac:
|
||||
needs:
|
||||
@@ -205,7 +211,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -585,6 +592,24 @@ jobs:
|
||||
|
||||
exit $EXIT_CODE
|
||||
shell: bash -euxo pipefail {0}
|
||||
call_autofix:
|
||||
needs:
|
||||
- check_style
|
||||
- run_tests_linux
|
||||
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: run_tests::call_autofix::dispatch_autofix
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.0"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -243,9 +243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.0"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -793,7 +793,7 @@ dependencies = [
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -7370,7 +7370,7 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.3",
|
||||
@@ -12648,6 +12648,8 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -18927,18 +18929,6 @@ dependencies = [
|
||||
"xcursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.9"
|
||||
@@ -18953,14 +18943,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -18973,7 +18963,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -293,6 +294,7 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -304,6 +306,7 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -267,6 +267,7 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -335,6 +336,7 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -347,6 +349,7 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -296,6 +297,7 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -308,6 +310,7 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1319,8 +1319,18 @@
|
||||
// Globs to match files that will be considered "hidden". These files can be hidden from the
|
||||
// project panel by toggling the "hide_hidden" setting.
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
// Git integration settings.
|
||||
"git": {
|
||||
// Master switch to disable all git integration features.
|
||||
// When true, all git features are disabled regardless of other settings.
|
||||
// When false (default), individual features are controlled by their respective settings.
|
||||
"disable_git": false,
|
||||
// Whether to show git status indicators (modified, added, deleted) in the
|
||||
// project panel, outline panel, and tabs.
|
||||
"enable_status": true,
|
||||
// Whether to show git diff information, including gutter diff indicators
|
||||
// and scrollbar diff markers.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
|
||||
@@ -192,6 +192,7 @@ pub struct ToolCall {
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -222,6 +223,11 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_input_markdown = tool_call
|
||||
.raw_input
|
||||
.as_ref()
|
||||
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
@@ -232,6 +238,7 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -307,6 +314,7 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
@@ -1355,6 +1363,7 @@ impl AcpThread {
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
|
||||
@@ -426,7 +426,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
|
||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -543,6 +543,9 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -553,133 +556,127 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content = buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -690,6 +687,13 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
&mut self,
|
||||
paths: Vec<project::ProjectPath>,
|
||||
@@ -967,6 +971,7 @@ impl Render for MessageEditor {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::paste_raw))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
.child({
|
||||
|
||||
@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models, favorites);
|
||||
info_list_to_picker_entries(filtered_models, &favorites);
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -406,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: Arc<HashSet<ModelId>>,
|
||||
favorites: &HashSet<ModelId>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -572,13 +572,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
||||
Arc::new(
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
@@ -609,7 +607,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -625,7 +623,7 @@ mod tests {
|
||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||
let favorites = create_favorites(vec![]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -641,7 +639,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
@@ -662,7 +660,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
@@ -683,7 +681,7 @@ mod tests {
|
||||
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let labels = get_entry_labels(&entries);
|
||||
|
||||
assert_eq!(
|
||||
@@ -723,7 +721,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
|
||||
@@ -34,7 +34,7 @@ use language::Buffer;
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||
@@ -260,6 +260,7 @@ impl ThreadFeedbackState {
|
||||
|
||||
pub struct AcpThreadView {
|
||||
agent: Rc<dyn AgentServer>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
@@ -337,7 +338,13 @@ impl AcpThreadView {
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let placeholder = placeholder_text(agent.name().as_ref(), false);
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let agent_display_name = agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(agent.name()))
|
||||
.unwrap_or_else(|| agent.name());
|
||||
|
||||
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
@@ -376,7 +383,6 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||
@@ -406,6 +412,7 @@ impl AcpThreadView {
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
agent_server_store,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
@@ -737,7 +744,7 @@ impl AcpThreadView {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let agent_name = agent.name();
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let sub = window.subscribe(®istry, cx, {
|
||||
@@ -779,7 +786,6 @@ impl AcpThreadView {
|
||||
configuration_view,
|
||||
description: err
|
||||
.description
|
||||
.clone()
|
||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||
_subscription: subscription,
|
||||
};
|
||||
@@ -1088,10 +1094,7 @@ impl AcpThreadView {
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
@@ -1500,7 +1503,13 @@ impl AcpThreadView {
|
||||
let has_commands = !available_commands.is_empty();
|
||||
self.available_commands.replace(available_commands);
|
||||
|
||||
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(&new_placeholder, window, cx);
|
||||
@@ -1663,44 +1672,6 @@ impl AcpThreadView {
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if method.0.as_ref() == "anthropic-api-key" {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let provider = registry
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -2153,6 +2124,7 @@ impl AcpThreadView {
|
||||
chunks,
|
||||
indented: _,
|
||||
}) => {
|
||||
let mut is_blank = true;
|
||||
let is_last = entry_ix + 1 == total_entries;
|
||||
|
||||
let style = default_markdown_style(false, false, window, cx);
|
||||
@@ -2162,36 +2134,55 @@ impl AcpThreadView {
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_first_indented, |this| this.pt_0p5())
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
if is_blank {
|
||||
Empty.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let has_terminals = tool_call.terminals().next().is_some();
|
||||
@@ -2223,7 +2214,7 @@ impl AcpThreadView {
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.pl(rems_from_px(20.0))
|
||||
.pl_5()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
@@ -2440,6 +2431,12 @@ impl AcpThreadView {
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let input_output_header = |label: SharedString| {
|
||||
Label::new(label)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
};
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2481,7 +2478,25 @@ impl AcpThreadView {
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Canceled => v_flex()
|
||||
.w_full()
|
||||
.when(!is_edit && !is_terminal_tool, |this| {
|
||||
this.mt_1p5().w_full().child(
|
||||
v_flex()
|
||||
.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
.child(input_output_header("Output:".into())),
|
||||
)
|
||||
})
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
@@ -2580,7 +2595,7 @@ impl AcpThreadView {
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
@@ -2766,20 +2781,20 @@ impl AcpThreadView {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.gap_2()
|
||||
.when(!card_layout, |this| {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.when(card_layout, |this| {
|
||||
this.px_2().pb_2().when(context_ix > 0, |this| {
|
||||
this.border_t_1()
|
||||
.pt_2()
|
||||
.map(|this| {
|
||||
if card_layout {
|
||||
this.when(context_ix > 0, |this| {
|
||||
this.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
} else {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
}
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
@@ -3500,138 +3515,119 @@ impl AcpThreadView {
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
) -> impl IntoElement {
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().status().warning.opacity(0.04))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("Authentication Required").size(LabelSize::Small)),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
div().text_ui(cx).child(self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
))
|
||||
}))
|
||||
.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.py_4()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.when(!show_description, |this| {
|
||||
this.border_t_1()
|
||||
.mt_1()
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let show_fallback_description = auth_methods.len() > 1
|
||||
&& configuration_view.is_none()
|
||||
&& description.is_none()
|
||||
&& pending_auth_method.is_none();
|
||||
|
||||
let auth_buttons = || {
|
||||
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
let (method_id, name) = if self
|
||||
.project
|
||||
.read(cx)
|
||||
.is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
.when_some(method.description.clone(), |this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
method.description.clone(),
|
||||
|this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
if pending_auth_method.is_some() {
|
||||
return Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticating to {}…", agent_display_name))
|
||||
.actions_slot(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticate to {}", agent_display_name))
|
||||
.when(auth_methods.len() == 1, |this| {
|
||||
this.actions_slot(auth_buttons())
|
||||
})
|
||||
.description_slot(
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.map(|this| {
|
||||
if show_fallback_description {
|
||||
this.child(
|
||||
Label::new("Choose one of the following authentication options:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.when(auth_methods.len() > 1, |this| {
|
||||
this.gap_1().child(auth_buttons())
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
@@ -5880,10 +5876,6 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
this.clear_thread_error(cx);
|
||||
if let Some(message) = this.in_flight_prompt.take() {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
@@ -5892,7 +5884,14 @@ impl AcpThreadView {
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -5905,14 +5904,10 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
self.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6015,16 +6010,19 @@ impl Render for AcpThreadView {
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => self
|
||||
.render_auth_required_state(
|
||||
} => v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.child(self.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
))
|
||||
.into_any_element(),
|
||||
ThreadState::Loading { .. } => v_flex()
|
||||
.flex_1()
|
||||
.child(self.render_recent_history(cx))
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
@@ -1698,6 +1698,9 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -1708,84 +1711,101 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1944,6 +1964,12 @@ impl TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste(&editor::actions::Paste, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2627,6 +2653,7 @@ impl Render for TextThreadEditor {
|
||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
||||
.on_action(cx.listener(TextThreadEditor::paste_raw))
|
||||
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
|
||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||
.on_action(cx.listener(TextThreadEditor::assist))
|
||||
|
||||
@@ -103,8 +103,9 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||
// Using the default value of None in the API instead
|
||||
Self::Chat | Self::Reasoner => None,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||
entity
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -348,6 +348,61 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.colorize_brackets = Some(true);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {r#"
|
||||
fn main() {
|
||||
let v: Vec<Stringˇ> = vec![];
|
||||
}
|
||||
"#});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec<String> = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"Markdown does not colorize <> brackets"
|
||||
);
|
||||
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec«2<String>2» = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"After switching to Rust, <> brackets are now colorized"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
|
||||
@@ -51,6 +51,8 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
|
||||
pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -179,7 +181,7 @@ impl CodeContextMenu {
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,26 +1421,6 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
fn as_debug_scenario(&self) -> Option<&DebugScenario> {
|
||||
let Self::DebugScenario(scenario) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(scenario)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
@@ -1446,6 +1428,14 @@ impl CodeActionsItem {
|
||||
Self::DebugScenario(scenario) => scenario.label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
|
||||
Self::Task(_, task) => task.resolved_label.replace("\n", ""),
|
||||
Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
@@ -1555,60 +1545,33 @@ impl CodeActionsMenu {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let colors = cx.theme().colors();
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_debug_scenario(), |this, scenario| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.overflow_x()
|
||||
.child(
|
||||
div()
|
||||
.min_w(CODE_ACTION_MENU_MIN_WIDTH)
|
||||
.max_w(CODE_ACTION_MENU_MAX_WIDTH)
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.when(selected, |this| this.text_color(colors.text_accent))
|
||||
.child(action.menu_label()),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
@@ -1635,4 +1598,42 @@ impl CodeActionsMenu {
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&mut self,
|
||||
max_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
let Some(action) = self.actions.get(self.selected_item) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let label = action.menu_label();
|
||||
let text_system = window.text_system();
|
||||
let mut line_wrapper = text_system.line_wrapper(
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated =
|
||||
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
div()
|
||||
.child(label)
|
||||
.id("code_actions_menu_extended")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,8 @@ impl Settings for EditorSettings {
|
||||
},
|
||||
scrollbar: Scrollbar {
|
||||
show: scrollbar.show.map(Into::into).unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap()
|
||||
&& content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
|
||||
selected_text: scrollbar.selected_text.unwrap(),
|
||||
selected_symbol: scrollbar.selected_symbol.unwrap(),
|
||||
search_results: scrollbar.search_results.unwrap(),
|
||||
|
||||
@@ -20880,6 +20880,36 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
ˇone
|
||||
- two
|
||||
three
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
one
|
||||
- two
|
||||
ˇthree
|
||||
- four
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.set_state(indoc! { "
|
||||
one
|
||||
ˇTWO
|
||||
@@ -20919,6 +20949,66 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggling_adjacent_diff_hunks_2(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_around_expanded_deletion_hunks(
|
||||
executor: BackgroundExecutor,
|
||||
|
||||
@@ -64,10 +64,7 @@ use project::{
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
project_settings::ProjectSettings,
|
||||
};
|
||||
use settings::{
|
||||
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||||
Settings,
|
||||
};
|
||||
use settings::{GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, Settings};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -2204,8 +2201,8 @@ impl EditorElement {
|
||||
.display_diff_hunks_for_rows(display_rows, folded_buffers)
|
||||
.map(|hunk| (hunk, None))
|
||||
.collect::<Vec<_>>();
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
|
||||
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
|
||||
let git_settings = &ProjectSettings::get_global(cx).git;
|
||||
if git_settings.is_gutter_enabled() {
|
||||
for (hunk, hitbox) in &mut display_hunks {
|
||||
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
|
||||
let hunk_bounds =
|
||||
@@ -6469,12 +6466,7 @@ impl EditorElement {
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
GitGutterSetting::TrackedFiles
|
||||
)
|
||||
});
|
||||
.unwrap_or_else(|| ProjectSettings::get_global(cx).git.is_gutter_enabled());
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
LICENSE-GPL
|
||||
../../LICENSE-GPL
|
||||
@@ -331,7 +331,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||
|
||||
mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.8.0",
|
||||
@@ -524,6 +523,12 @@ impl wit::Guest for Component {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct LanguageServerId(String);
|
||||
|
||||
impl LanguageServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for LanguageServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
@@ -540,6 +545,12 @@ impl fmt::Display for LanguageServerId {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct ContextServerId(String);
|
||||
|
||||
impl ContextServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContextServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
|
||||
@@ -58,7 +58,7 @@ use project::{
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
project_settings::{GitPathStyle, ProjectSettings},
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, StatusStyle};
|
||||
use std::future::Future;
|
||||
@@ -2579,25 +2579,26 @@ impl GitPanel {
|
||||
is_using_legacy_zed_pro: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> String {
|
||||
const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
// Remove this once we stop supporting legacy Zed Pro
|
||||
// In legacy Zed Pro, Git commit summary generation did not count as a
|
||||
// prompt. If the user changes the prompt, our classification will fail,
|
||||
// meaning that users will be charged for generating commit messages.
|
||||
if is_using_legacy_zed_pro {
|
||||
return DEFAULT_PROMPT.to_string();
|
||||
return BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
}
|
||||
|
||||
let load = async {
|
||||
let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?;
|
||||
store
|
||||
.update(cx, |s, cx| s.load(PromptId::CommitMessage, cx))
|
||||
.update(cx, |s, cx| {
|
||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()
|
||||
};
|
||||
load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string())
|
||||
load.await
|
||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
||||
}
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
|
||||
@@ -566,22 +566,22 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Gemini25FlashLite => 1_048_576,
|
||||
Self::Gemini25Flash => 1_048_576,
|
||||
Self::Gemini25Pro => 1_048_576,
|
||||
Self::Gemini3Pro => 1_048_576,
|
||||
Self::Gemini3Flash => 1_048_576,
|
||||
Self::Gemini25FlashLite
|
||||
| Self::Gemini25Flash
|
||||
| Self::Gemini25Pro
|
||||
| Self::Gemini3Pro
|
||||
| Self::Gemini3Flash => 1_048_576,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Model::Gemini25FlashLite => Some(65_536),
|
||||
Model::Gemini25Flash => Some(65_536),
|
||||
Model::Gemini25Pro => Some(65_536),
|
||||
Model::Gemini3Pro => Some(65_536),
|
||||
Model::Gemini3Flash => Some(65_536),
|
||||
Model::Gemini25FlashLite
|
||||
| Model::Gemini25Flash
|
||||
| Model::Gemini25Pro
|
||||
| Model::Gemini3Pro
|
||||
| Model::Gemini3Flash => Some(65_536),
|
||||
Model::Custom { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.2", optional = true }
|
||||
wayland-cursor = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.31.2", features = [
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
|
||||
@@ -5,6 +5,7 @@ use gpui::{
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
is_dialog: bool,
|
||||
}
|
||||
|
||||
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
|
||||
@@ -23,7 +24,10 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
|
||||
}
|
||||
|
||||
impl Render for SubWindow {
|
||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -52,8 +56,28 @@ impl Render for SubWindow {
|
||||
.child(
|
||||
div()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child("SubWindow")
|
||||
.when(self.is_dialog, |div| {
|
||||
div.child(button("Open Nested Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
})
|
||||
.child(button("Close", |window, _| {
|
||||
window.remove_window();
|
||||
})),
|
||||
@@ -86,6 +110,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -101,6 +126,39 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Floating", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Floating,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Dialog", move |_, cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::Dialog,
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -116,6 +174,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: true,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -131,6 +190,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -147,6 +207,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -162,6 +223,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -177,6 +239,7 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -316,6 +316,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "next" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let next_idx = (idx + 1) % group_ids.len();
|
||||
@@ -340,6 +341,7 @@ impl SystemWindowTabController {
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
|
||||
|
||||
let current_group = current_group?;
|
||||
// TODO: `.keys()` returns arbitrary order, what does "previous" mean?
|
||||
let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
|
||||
let idx = group_ids.iter().position(|g| *g == current_group)?;
|
||||
let prev_idx = if idx == 0 {
|
||||
@@ -361,12 +363,9 @@ impl SystemWindowTabController {
|
||||
|
||||
/// Get all tabs in the same window.
|
||||
pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
|
||||
let tab_group = self
|
||||
.tab_groups
|
||||
.iter()
|
||||
.find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
|
||||
|
||||
self.tab_groups.get(&tab_group)
|
||||
self.tab_groups
|
||||
.values()
|
||||
.find(|tabs| tabs.iter().any(|tab| tab.id == id))
|
||||
}
|
||||
|
||||
/// Initialize the visibility of the system window tab controller.
|
||||
@@ -441,7 +440,7 @@ impl SystemWindowTabController {
|
||||
/// Insert a tab into a tab group.
|
||||
pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
|
||||
let mut controller = cx.global_mut::<SystemWindowTabController>();
|
||||
let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
|
||||
let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -504,16 +503,14 @@ impl SystemWindowTabController {
|
||||
return;
|
||||
};
|
||||
|
||||
let initial_tabs_len = initial_tabs.len();
|
||||
let mut all_tabs = initial_tabs.clone();
|
||||
for tabs in controller.tab_groups.values() {
|
||||
all_tabs.extend(
|
||||
tabs.iter()
|
||||
.filter(|tab| !initial_tabs.contains(tab))
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
for (_, mut tabs) in controller.tab_groups.drain() {
|
||||
tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
|
||||
all_tabs.extend(tabs);
|
||||
}
|
||||
|
||||
controller.tab_groups.clear();
|
||||
controller.tab_groups.insert(0, all_tabs);
|
||||
}
|
||||
|
||||
|
||||
@@ -1348,6 +1348,10 @@ pub enum WindowKind {
|
||||
/// docks, notifications or wallpapers.
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
LayerShell(layer_shell::LayerShellOptions),
|
||||
|
||||
/// A window that appears on top of its parent window and blocks interaction with it
|
||||
/// until the modal window is closed
|
||||
Dialog,
|
||||
}
|
||||
|
||||
/// The appearance of the window, as defined by the operating system.
|
||||
|
||||
@@ -36,12 +36,6 @@ use wayland_client::{
|
||||
wl_shm_pool, wl_surface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
|
||||
self, ZwpPrimarySelectionOfferV1,
|
||||
};
|
||||
@@ -61,6 +55,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols::{
|
||||
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
|
||||
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
|
||||
};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
|
||||
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
@@ -122,6 +124,7 @@ pub struct Globals {
|
||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
@@ -132,6 +135,7 @@ impl Globals {
|
||||
qh: QueueHandle<WaylandClientStatePtr>,
|
||||
seat: wl_seat::WlSeat,
|
||||
) -> Self {
|
||||
let dialog_v = XdgWmDialogV1::interface().version;
|
||||
Globals {
|
||||
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
compositor: globals
|
||||
@@ -160,6 +164,7 @@ impl Globals {
|
||||
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
@@ -729,10 +734,7 @@ impl LinuxClient for WaylandClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let parent = state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.and_then(|w| w.toplevel());
|
||||
let parent = state.keyboard_focused_window.clone();
|
||||
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
@@ -751,7 +753,12 @@ impl LinuxClient for WaylandClient {
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let need_update = state.cursor_style != Some(style);
|
||||
let need_update = state.cursor_style != Some(style)
|
||||
&& (state.mouse_focused_window.is_none()
|
||||
|| state
|
||||
.mouse_focused_window
|
||||
.as_ref()
|
||||
.is_some_and(|w| !w.is_blocked()));
|
||||
|
||||
if need_update {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
@@ -1011,7 +1018,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window(
|
||||
pub(crate) fn get_window(
|
||||
mut state: &mut RefMut<WaylandClientState>,
|
||||
surface_id: &ObjectId,
|
||||
) -> Option<WaylandWindowStatePtr> {
|
||||
@@ -1654,6 +1661,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
if window.is_blocked() {
|
||||
let default_style = CursorStyle::Arrow;
|
||||
if state.cursor_style != Some(default_style) {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
state.cursor_style = Some(default_style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, default_style.to_shape());
|
||||
} else {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
let scale = window.primary_output_scale();
|
||||
state.cursor.set_icon(
|
||||
&wl_pointer,
|
||||
serial,
|
||||
default_style.to_icon_names(),
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
@@ -2225,3 +2256,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &XdgWmDialogV1,
|
||||
_: <XdgWmDialogV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &XdgDialogV1,
|
||||
_event: <XdgDialogV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use collections::{FxHashSet, HashMap};
|
||||
use futures::channel::oneshot::Receiver;
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols::{
|
||||
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
|
||||
xdg::shell::client::xdg_toplevel::XdgToplevel,
|
||||
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
|
||||
@@ -29,7 +29,7 @@ use crate::{
|
||||
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
|
||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
|
||||
layer_shell::LayerShellNotSupportedError, px, size,
|
||||
};
|
||||
use crate::{
|
||||
@@ -87,6 +87,8 @@ struct InProgressConfigure {
|
||||
pub struct WaylandWindowState {
|
||||
surface_state: WaylandSurfaceState,
|
||||
acknowledged_first_configure: bool,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
children: FxHashSet<ObjectId>,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
@@ -126,7 +128,7 @@ impl WaylandSurfaceState {
|
||||
surface: &wl_surface::WlSurface,
|
||||
globals: &Globals,
|
||||
params: &WindowParams,
|
||||
parent: Option<XdgToplevel>,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// For layer_shell windows, create a layer surface instead of an xdg surface
|
||||
if let WindowKind::LayerShell(options) = ¶ms.kind {
|
||||
@@ -178,10 +180,28 @@ impl WaylandSurfaceState {
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
if params.kind == WindowKind::Floating {
|
||||
toplevel.set_parent(parent.as_ref());
|
||||
let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
|
||||
|
||||
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||
toplevel.set_parent(xdg_parent.as_ref());
|
||||
}
|
||||
|
||||
let dialog = if params.kind == WindowKind::Dialog {
|
||||
let dialog = globals.dialog.as_ref().map(|dialog| {
|
||||
let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
|
||||
xdg_dialog.set_modal();
|
||||
xdg_dialog
|
||||
});
|
||||
|
||||
if let Some(parent) = parent.as_ref() {
|
||||
parent.add_child(surface.id());
|
||||
}
|
||||
|
||||
dialog
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
@@ -198,6 +218,7 @@ impl WaylandSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
dialog,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -206,6 +227,7 @@ pub struct WaylandXdgSurfaceState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
dialog: Option<XdgDialogV1>,
|
||||
}
|
||||
|
||||
pub struct WaylandLayerSurfaceState {
|
||||
@@ -258,7 +280,13 @@ impl WaylandSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration: _decoration,
|
||||
dialog,
|
||||
}) => {
|
||||
// drop the dialog before toplevel so compositor can explicitly unapply it's effects
|
||||
if let Some(dialog) = dialog {
|
||||
dialog.destroy();
|
||||
}
|
||||
|
||||
// The role object (toplevel) must always be destroyed before the xdg_surface.
|
||||
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
|
||||
toplevel.destroy();
|
||||
@@ -288,6 +316,7 @@ impl WaylandWindowState {
|
||||
globals: Globals,
|
||||
gpu_context: &BladeContext,
|
||||
options: WindowParams,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let renderer = {
|
||||
let raw_window = RawWindow {
|
||||
@@ -319,6 +348,8 @@ impl WaylandWindowState {
|
||||
Ok(Self {
|
||||
surface_state,
|
||||
acknowledged_first_configure: false,
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
surface,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
@@ -391,6 +422,10 @@ impl Drop for WaylandWindow {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
let surface_id = state.surface.id();
|
||||
if let Some(parent) = state.parent.as_ref() {
|
||||
parent.state.borrow_mut().children.remove(&surface_id);
|
||||
}
|
||||
|
||||
let client = state.client.clone();
|
||||
|
||||
state.renderer.destroy();
|
||||
@@ -448,10 +483,10 @@ impl WaylandWindow {
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
appearance: WindowAppearance,
|
||||
parent: Option<XdgToplevel>,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
) -> anyhow::Result<(Self, ObjectId)> {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent)?;
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone())?;
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -473,6 +508,7 @@ impl WaylandWindow {
|
||||
globals,
|
||||
gpu_context,
|
||||
params,
|
||||
parent,
|
||||
)?)),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
});
|
||||
@@ -501,6 +537,16 @@ impl WaylandWindowStatePtr {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
pub fn add_child(&self, child: ObjectId) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.children.insert(child);
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
!state.children.is_empty()
|
||||
}
|
||||
|
||||
pub fn frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
@@ -818,6 +864,9 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime(&self, ime: ImeInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -894,6 +943,21 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let state = self.state.borrow();
|
||||
let client = state.client.get_client();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let children = state.children.clone();
|
||||
drop(state);
|
||||
|
||||
for child in children {
|
||||
let mut client_state = client.borrow_mut();
|
||||
let window = get_window(&mut client_state, &child);
|
||||
drop(client_state);
|
||||
|
||||
if let Some(child) = window {
|
||||
child.close();
|
||||
}
|
||||
}
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
@@ -901,6 +965,9 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||
&& !fun(input.clone()).propagate
|
||||
{
|
||||
|
||||
@@ -222,7 +222,7 @@ pub struct X11ClientState {
|
||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11ClientStatePtr {
|
||||
fn get_client(&self) -> Option<X11Client> {
|
||||
pub fn get_client(&self) -> Option<X11Client> {
|
||||
self.0.upgrade().map(X11Client)
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
.windows
|
||||
@@ -789,12 +789,12 @@ impl X11Client {
|
||||
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
|
||||
// window "x" button clicked by user
|
||||
if window.should_close() {
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
drop(state);
|
||||
window.close();
|
||||
state = self.0.borrow_mut();
|
||||
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
||||
window.state.borrow_mut().last_sync_counter =
|
||||
Some(x11rb::protocol::sync::Int64 {
|
||||
@@ -1216,6 +1216,33 @@ impl X11Client {
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
if window.is_blocked() {
|
||||
// We want to set the cursor to the default arrow
|
||||
// when the window is blocked
|
||||
let style = CursorStyle::Arrow;
|
||||
|
||||
let current_style = state
|
||||
.cursor_styles
|
||||
.get(&window.x_window)
|
||||
.unwrap_or(&CursorStyle::Arrow);
|
||||
if *current_style != style
|
||||
&& let Some(cursor) = state.get_cursor_icon(style)
|
||||
{
|
||||
state.cursor_styles.insert(window.x_window, style);
|
||||
check_reply(
|
||||
|| "Failed to set cursor style",
|
||||
state.xcb_connection.change_window_attributes(
|
||||
window.x_window,
|
||||
&ChangeWindowAttributesAux {
|
||||
cursor: Some(cursor),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
)
|
||||
.log_err();
|
||||
state.xcb_connection.flush().log_err();
|
||||
};
|
||||
}
|
||||
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -1489,7 +1516,7 @@ impl LinuxClient for X11Client {
|
||||
let parent_window = state
|
||||
.keyboard_focused_window
|
||||
.and_then(|focused_window| state.windows.get(&focused_window))
|
||||
.map(|window| window.window.x_window);
|
||||
.map(|w| w.window.clone());
|
||||
let x_window = state
|
||||
.xcb_connection
|
||||
.generate_id()
|
||||
@@ -1544,7 +1571,15 @@ impl LinuxClient for X11Client {
|
||||
.cursor_styles
|
||||
.get(&focused_window)
|
||||
.unwrap_or(&CursorStyle::Arrow);
|
||||
if *current_style == style {
|
||||
|
||||
let window = state
|
||||
.mouse_focused_window
|
||||
.and_then(|w| state.windows.get(&w));
|
||||
|
||||
let should_change = *current_style != style
|
||||
&& (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
|
||||
|
||||
if !should_change {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::FxHashSet;
|
||||
use raw_window_handle as rwh;
|
||||
use util::{ResultExt, maybe};
|
||||
use x11rb::{
|
||||
@@ -74,6 +75,7 @@ x11rb::atom_manager! {
|
||||
_NET_WM_WINDOW_TYPE,
|
||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
_NET_WM_WINDOW_TYPE_DIALOG,
|
||||
_NET_WM_STATE_MODAL,
|
||||
_NET_WM_SYNC,
|
||||
_NET_SUPPORTED,
|
||||
_MOTIF_WM_HINTS,
|
||||
@@ -249,6 +251,8 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
parent: Option<X11WindowStatePtr>,
|
||||
children: FxHashSet<xproto::Window>,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -394,7 +398,7 @@ impl X11WindowState {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<xproto::Window>,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let x_screen_index = params
|
||||
.display_id
|
||||
@@ -546,8 +550,8 @@ impl X11WindowState {
|
||||
)?;
|
||||
}
|
||||
|
||||
if params.kind == WindowKind::Floating {
|
||||
if let Some(parent_window) = parent_window {
|
||||
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
|
||||
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
|
||||
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
|
||||
// place the floating window in relation to the main window.
|
||||
@@ -563,11 +567,23 @@ impl X11WindowState {
|
||||
),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
let parent = if params.kind == WindowKind::Dialog
|
||||
&& let Some(parent) = parent_window
|
||||
{
|
||||
parent.add_child(x_window);
|
||||
|
||||
Some(parent)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if params.kind == WindowKind::Dialog {
|
||||
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
|
||||
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
|
||||
check_reply(
|
||||
|| "X11 ChangeProperty32 setting window type for floating window failed.",
|
||||
|| "X11 ChangeProperty32 setting window type for dialog window failed.",
|
||||
xcb.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
@@ -576,6 +592,20 @@ impl X11WindowState {
|
||||
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
|
||||
),
|
||||
)?;
|
||||
|
||||
// We set the modal state for dialog windows, so that the window manager
|
||||
// can handle it appropriately (e.g., prevent interaction with the parent window
|
||||
// while the dialog is open).
|
||||
check_reply(
|
||||
|| "X11 ChangeProperty32 setting modal state for dialog window failed.",
|
||||
xcb.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
atoms._NET_WM_STATE,
|
||||
xproto::AtomEnum::ATOM,
|
||||
&[atoms._NET_WM_STATE_MODAL],
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
||||
check_reply(
|
||||
@@ -667,6 +697,8 @@ impl X11WindowState {
|
||||
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
||||
|
||||
Ok(Self {
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
client,
|
||||
executor,
|
||||
display,
|
||||
@@ -720,6 +752,11 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
|
||||
impl Drop for X11Window {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
if let Some(parent) = state.parent.as_ref() {
|
||||
parent.state.borrow_mut().children.remove(&self.0.x_window);
|
||||
}
|
||||
|
||||
state.renderer.destroy();
|
||||
|
||||
let destroy_x_window = maybe!({
|
||||
@@ -734,8 +771,6 @@ impl Drop for X11Window {
|
||||
.log_err();
|
||||
|
||||
if destroy_x_window.is_some() {
|
||||
// Mark window as destroyed so that we can filter out when X11 events
|
||||
// for it still come in.
|
||||
state.destroyed = true;
|
||||
|
||||
let this_ptr = self.0.clone();
|
||||
@@ -773,7 +808,7 @@ impl X11Window {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<xproto::Window>,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let ptr = X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
@@ -979,7 +1014,31 @@ impl X11WindowStatePtr {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_child(&self, child: xproto::Window) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.children.insert(child);
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
!state.children.is_empty()
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let state = self.state.borrow();
|
||||
let client = state.client.clone();
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let children = state.children.clone();
|
||||
drop(state);
|
||||
|
||||
if let Some(client) = client.get_client() {
|
||||
for child in children {
|
||||
if let Some(child_window) = client.get_window(child) {
|
||||
child_window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
@@ -994,6 +1053,9 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||
&& !fun(input.clone()).propagate
|
||||
{
|
||||
@@ -1016,6 +1078,9 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_commit(&self, text: String) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1026,6 +1091,9 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_preedit(&self, text: String) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1036,6 +1104,9 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_unmark(&self) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
@@ -1046,6 +1117,9 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_ime_delete(&self) {
|
||||
if self.is_blocked() {
|
||||
return;
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
MetalRenderer::new(context, transparent)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,8 +152,13 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
// We already present at display sync with the display link
|
||||
// This allows to use direct-to-display even in window mode
|
||||
layer.set_display_sync_enabled(false);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -352,8 +357,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -62,9 +62,12 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||||
NSWindowStyleMask::from_bits_retain(1 << 7);
|
||||
// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSNormalWindowLevel: NSInteger = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSFloatingWindowLevel: NSInteger = 3;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSPopUpWindowLevel: NSInteger = 101;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
|
||||
@@ -423,6 +426,8 @@ struct MacWindowState {
|
||||
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
|
||||
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
|
||||
activated_least_once: bool,
|
||||
// The parent window if this window is a sheet (Dialog kind)
|
||||
sheet_parent: Option<id>,
|
||||
}
|
||||
|
||||
impl MacWindowState {
|
||||
@@ -622,11 +627,16 @@ impl MacWindow {
|
||||
}
|
||||
|
||||
let native_window: id = match kind {
|
||||
WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::Normal => {
|
||||
msg_send![WINDOW_CLASS, alloc]
|
||||
}
|
||||
WindowKind::PopUp => {
|
||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
}
|
||||
WindowKind::Floating | WindowKind::Dialog => {
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
}
|
||||
};
|
||||
|
||||
let display = display_id
|
||||
@@ -729,6 +739,7 @@ impl MacWindow {
|
||||
select_previous_tab_callback: None,
|
||||
toggle_tab_bar_callback: None,
|
||||
activated_least_once: false,
|
||||
sheet_parent: None,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
@@ -779,9 +790,18 @@ impl MacWindow {
|
||||
content_view.addSubview_(native_view.autorelease());
|
||||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
let app: id = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
let mut sheet_parent = None;
|
||||
|
||||
match kind {
|
||||
WindowKind::Normal | WindowKind::Floating => {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
if kind == WindowKind::Floating {
|
||||
// Let the window float keep above normal windows.
|
||||
native_window.setLevel_(NSFloatingWindowLevel);
|
||||
} else {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
}
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
|
||||
if let Some(tabbing_identifier) = tabbing_identifier {
|
||||
@@ -816,10 +836,23 @@ impl MacWindow {
|
||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
|
||||
);
|
||||
}
|
||||
WindowKind::Dialog => {
|
||||
if !main_window.is_null() {
|
||||
let parent = {
|
||||
let active_sheet: id = msg_send![main_window, attachedSheet];
|
||||
if active_sheet.is_null() {
|
||||
main_window
|
||||
} else {
|
||||
active_sheet
|
||||
}
|
||||
};
|
||||
let _: () =
|
||||
msg_send![parent, beginSheet: native_window completionHandler: nil];
|
||||
sheet_parent = Some(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let main_window: id = msg_send![app, mainWindow];
|
||||
if allows_automatic_window_tabbing
|
||||
&& !main_window.is_null()
|
||||
&& main_window != native_window
|
||||
@@ -861,7 +894,11 @@ impl MacWindow {
|
||||
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
|
||||
// is different from the primary screen.
|
||||
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
|
||||
window.0.lock().move_traffic_light();
|
||||
{
|
||||
let mut window_state = window.0.lock();
|
||||
window_state.move_traffic_light();
|
||||
window_state.sheet_parent = sheet_parent;
|
||||
}
|
||||
|
||||
pool.drain();
|
||||
|
||||
@@ -938,6 +975,7 @@ impl Drop for MacWindow {
|
||||
let mut this = self.0.lock();
|
||||
this.renderer.destroy();
|
||||
let window = this.native_window;
|
||||
let sheet_parent = this.sheet_parent.take();
|
||||
this.display_link.take();
|
||||
unsafe {
|
||||
this.native_window.setDelegate_(nil);
|
||||
@@ -946,6 +984,9 @@ impl Drop for MacWindow {
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
if let Some(parent) = sheet_parent {
|
||||
let _: () = msg_send![parent, endSheet: window];
|
||||
}
|
||||
window.close();
|
||||
window.autorelease();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ impl WindowsWindowInner {
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
let handled = match msg {
|
||||
// eagerly activate the window, so calls to `active_window` will work correctly
|
||||
WM_MOUSEACTIVATE => {
|
||||
unsafe { SetActiveWindow(handle).log_err() };
|
||||
None
|
||||
}
|
||||
WM_ACTIVATE => self.handle_activate_msg(wparam),
|
||||
WM_CREATE => self.handle_create_msg(handle),
|
||||
WM_MOVE => self.handle_move_msg(handle, lparam),
|
||||
@@ -265,6 +270,14 @@ impl WindowsWindowInner {
|
||||
|
||||
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
|
||||
let callback = { self.state.callbacks.close.take() };
|
||||
// Re-enable parent window if this was a modal dialog
|
||||
if let Some(parent_hwnd) = self.parent_hwnd {
|
||||
unsafe {
|
||||
let _ = EnableWindow(parent_hwnd, true);
|
||||
let _ = SetForegroundWindow(parent_hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
|
||||
if let Err(err) = result {
|
||||
// ERROR_NOT_FOUND means the credential doesn't exist.
|
||||
// Return Ok(None) to match macOS and Linux behavior.
|
||||
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
|
||||
if err.code() == ERROR_NOT_FOUND.to_hresult() {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(err.into());
|
||||
|
||||
@@ -83,6 +83,7 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(crate) validation_number: usize,
|
||||
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
|
||||
pub(crate) platform_window_handle: HWND,
|
||||
pub(crate) parent_hwnd: Option<HWND>,
|
||||
}
|
||||
|
||||
impl WindowsWindowState {
|
||||
@@ -241,6 +242,7 @@ impl WindowsWindowInner {
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
platform_window_handle: context.platform_window_handle,
|
||||
system_settings: WindowsSystemSettings::new(context.display),
|
||||
parent_hwnd: context.parent_hwnd,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -368,6 +370,7 @@ struct WindowCreateContext {
|
||||
disable_direct_composition: bool,
|
||||
directx_devices: DirectXDevices,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
parent_hwnd: Option<HWND>,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -390,6 +393,20 @@ impl WindowsWindow {
|
||||
invalidate_devices,
|
||||
} = creation_info;
|
||||
register_window_class(icon);
|
||||
let parent_hwnd = if params.kind == WindowKind::Dialog {
|
||||
let parent_window = unsafe { GetActiveWindow() };
|
||||
if parent_window.is_invalid() {
|
||||
None
|
||||
} else {
|
||||
// Disable the parent window to make this dialog modal
|
||||
unsafe {
|
||||
EnableWindow(parent_window, false).as_bool();
|
||||
};
|
||||
Some(parent_window)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let hide_title_bar = params
|
||||
.titlebar
|
||||
.as_ref()
|
||||
@@ -416,8 +433,14 @@ impl WindowsWindow {
|
||||
if params.is_minimizable {
|
||||
dwstyle |= WS_MINIMIZEBOX;
|
||||
}
|
||||
let dwexstyle = if params.kind == WindowKind::Dialog {
|
||||
dwstyle |= WS_POPUP | WS_CAPTION;
|
||||
WS_EX_DLGMODALFRAME
|
||||
} else {
|
||||
WS_EX_APPWINDOW
|
||||
};
|
||||
|
||||
(WS_EX_APPWINDOW, dwstyle)
|
||||
(dwexstyle, dwstyle)
|
||||
};
|
||||
if !disable_direct_composition {
|
||||
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
||||
@@ -449,6 +472,7 @@ impl WindowsWindow {
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
parent_hwnd,
|
||||
};
|
||||
let creation_result = unsafe {
|
||||
CreateWindowExW(
|
||||
@@ -460,7 +484,7 @@ impl WindowsWindow {
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
None,
|
||||
parent_hwnd,
|
||||
None,
|
||||
Some(hinstance.into()),
|
||||
Some(&context as *const _ as *const _),
|
||||
|
||||
@@ -128,22 +128,21 @@ impl LineWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
/// Determines if a line should be truncated based on its width.
|
||||
pub fn should_truncate_line(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
line: &str,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
let mut suffix_width = truncation_suffix
|
||||
let suffix_width = truncation_suffix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut char_indices = line.char_indices();
|
||||
let mut truncate_ix = 0;
|
||||
for (ix, c) in char_indices {
|
||||
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
@@ -152,16 +151,32 @@ impl LineWrapper {
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
|
||||
return (result, Cow::Owned(runs));
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
|
||||
(line, Cow::Borrowed(runs))
|
||||
None
|
||||
}
|
||||
|
||||
/// Truncate a line of text to the given width with this wrapper's font and font size.
|
||||
pub fn truncate_line<'a>(
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_suffix)
|
||||
{
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Any character in this list should be treated as a word character,
|
||||
|
||||
@@ -4966,7 +4966,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
|
||||
}
|
||||
|
||||
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct AnyWindowHandle {
|
||||
pub(crate) id: WindowId,
|
||||
state_type: TypeId,
|
||||
|
||||
@@ -1801,9 +1801,7 @@ impl Buffer {
|
||||
self.syntax_map.lock().did_parse(syntax_snapshot);
|
||||
self.request_autoindent(cx);
|
||||
self.parse_status.0.send(ParseStatus::Idle).unwrap();
|
||||
if self.text.version() != *self.tree_sitter_data.version() {
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
}
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
cx.emit(BufferEvent::Reparsed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -295,6 +295,23 @@ impl LspInstaller for TyLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: Option<Toolchain>,
|
||||
_: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
|
||||
return None;
|
||||
};
|
||||
let env = delegate.shell_env().await;
|
||||
Some(LanguageServerBinary {
|
||||
path: ty_bin,
|
||||
env: Some(env),
|
||||
arguments: vec!["server".into()],
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Self::BinaryVersion,
|
||||
|
||||
@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
| lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
|
||||
) = completion.text_edit.as_ref()
|
||||
&& let Ok(mut snippet) = snippet::Snippet::parse(new_text)
|
||||
&& !snippet.tabstops.is_empty()
|
||||
&& snippet.tabstops.len() > 1
|
||||
{
|
||||
label = String::new();
|
||||
|
||||
@@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter {
|
||||
0..label.rfind('(').unwrap_or(completion.label.len()),
|
||||
highlight_id,
|
||||
));
|
||||
} else if detail_left.is_none() {
|
||||
} else if detail_left.is_none()
|
||||
&& kind != Some(lsp::CompletionItemKind::SNIPPET)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1597,6 +1599,40 @@ mod tests {
|
||||
))
|
||||
);
|
||||
|
||||
// Postfix completion without actual tabstops (only implicit final $0)
|
||||
// The label should use completion.label so it can be filtered by "ref"
|
||||
let ref_completion = adapter
|
||||
.label_for_completion(
|
||||
&lsp::CompletionItem {
|
||||
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||
label: "ref".to_string(),
|
||||
filter_text: Some("ref".to_string()),
|
||||
label_details: Some(CompletionItemLabelDetails {
|
||||
detail: None,
|
||||
description: Some("&expr".to_string()),
|
||||
}),
|
||||
detail: Some("&expr".to_string()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::default(),
|
||||
new_text: "&String::new()".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
&language,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
ref_completion.is_some(),
|
||||
"ref postfix completion should have a label"
|
||||
);
|
||||
let ref_label = ref_completion.unwrap();
|
||||
let filter_text = &ref_label.text[ref_label.filter_range.clone()];
|
||||
assert!(
|
||||
filter_text.contains("ref"),
|
||||
"filter range text '{filter_text}' should contain 'ref' for filtering to work",
|
||||
);
|
||||
|
||||
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
|
||||
let res = adapter
|
||||
.label_for_completion(
|
||||
|
||||
@@ -155,15 +155,15 @@ impl Model {
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::CodestralLatest => 256000,
|
||||
Self::MistralLargeLatest => 131000,
|
||||
Self::MistralLargeLatest => 256000,
|
||||
Self::MistralMediumLatest => 128000,
|
||||
Self::MistralSmallLatest => 32000,
|
||||
Self::MagistralMediumLatest => 40000,
|
||||
Self::MagistralSmallLatest => 40000,
|
||||
Self::MagistralMediumLatest => 128000,
|
||||
Self::MagistralSmallLatest => 128000,
|
||||
Self::OpenMistralNemo => 131000,
|
||||
Self::OpenCodestralMamba => 256000,
|
||||
Self::DevstralMediumLatest => 128000,
|
||||
Self::DevstralSmallLatest => 262144,
|
||||
Self::DevstralMediumLatest => 256000,
|
||||
Self::DevstralSmallLatest => 256000,
|
||||
Self::Pixtral12BLatest => 128000,
|
||||
Self::PixtralLargeLatest => 128000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
|
||||
@@ -2610,9 +2610,8 @@ impl MultiBuffer {
|
||||
for range in ranges {
|
||||
let range = range.to_point(&snapshot);
|
||||
let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
|
||||
let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0));
|
||||
let start = start.saturating_sub_usize(1);
|
||||
let end = snapshot.len().min(end + 1usize);
|
||||
let end = (snapshot.point_to_offset(Point::new(range.end.row + 1, 0)) + 1usize)
|
||||
.min(snapshot.len());
|
||||
cursor.seek(&start, Bias::Right);
|
||||
while let Some(item) = cursor.item() {
|
||||
if *cursor.start() >= end {
|
||||
|
||||
@@ -50,7 +50,13 @@ impl Settings for OutlinePanelSettings {
|
||||
dock: panel.dock.unwrap(),
|
||||
file_icons: panel.file_icons.unwrap(),
|
||||
folder_icons: panel.folder_icons.unwrap(),
|
||||
git_status: panel.git_status.unwrap(),
|
||||
git_status: panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -460,7 +460,7 @@ impl AgentServerStore {
|
||||
.gemini
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(false),
|
||||
.unwrap_or(true),
|
||||
}),
|
||||
);
|
||||
self.external_agents.insert(
|
||||
|
||||
@@ -1672,59 +1672,6 @@ impl GitStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_entries_pending_by_project_paths(
|
||||
&mut self,
|
||||
project_paths: &[ProjectPath],
|
||||
stage: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer_store = &self.buffer_store;
|
||||
|
||||
for project_path in project_paths {
|
||||
let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let Some(diff_state) = self.diffs.get(&buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
diff_state.update(cx, |diff_state, cx| {
|
||||
let Some(uncommitted_diff) = diff_state.uncommitted_diff() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||
let file_exists = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().exists());
|
||||
|
||||
let all_hunks: Vec<_> = uncommitted_diff
|
||||
.read(cx)
|
||||
.hunks_intersecting_range(
|
||||
text::Anchor::MIN..text::Anchor::MAX,
|
||||
&buffer_snapshot,
|
||||
cx,
|
||||
)
|
||||
.collect();
|
||||
|
||||
if !all_hunks.is_empty() {
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
diff.stage_or_unstage_hunks(
|
||||
stage,
|
||||
&all_hunks,
|
||||
&buffer_snapshot,
|
||||
file_exists,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git_clone(
|
||||
&self,
|
||||
repo: String,
|
||||
@@ -4253,28 +4200,6 @@ impl Repository {
|
||||
save_futures
|
||||
}
|
||||
|
||||
fn mark_entries_pending_for_stage(
|
||||
&self,
|
||||
entries: &[RepoPath],
|
||||
stage: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(git_store) = self.git_store() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut project_paths = Vec::new();
|
||||
for repo_path in entries {
|
||||
if let Some(project_path) = self.repo_path_to_project_path(repo_path, cx) {
|
||||
project_paths.push(project_path);
|
||||
}
|
||||
}
|
||||
|
||||
git_store.update(cx, move |git_store, cx| {
|
||||
git_store.mark_entries_pending_by_project_paths(&project_paths, stage, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stage_entries(
|
||||
&mut self,
|
||||
entries: Vec<RepoPath>,
|
||||
@@ -4283,9 +4208,6 @@ impl Repository {
|
||||
if entries.is_empty() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
self.mark_entries_pending_for_stage(&entries, true, cx);
|
||||
|
||||
let id = self.id;
|
||||
let save_tasks = self.save_buffers(&entries, cx);
|
||||
let paths = entries
|
||||
@@ -4351,9 +4273,6 @@ impl Repository {
|
||||
if entries.is_empty() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
self.mark_entries_pending_for_stage(&entries, false, cx);
|
||||
|
||||
let id = self.id;
|
||||
let save_tasks = self.save_buffers(&entries, cx);
|
||||
let paths = entries
|
||||
|
||||
@@ -332,6 +332,10 @@ impl GoToDiagnosticSeverityFilter {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not git integration is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
pub enabled: GitEnabledSettings,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -361,6 +365,39 @@ pub struct GitSettings {
|
||||
pub path_style: GitPathStyle,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
/// Returns whether git status indicators should be shown.
|
||||
/// This includes status in the project panel, outline panel, and tabs.
|
||||
pub fn is_status_enabled(&self) -> bool {
|
||||
self.enabled.status
|
||||
}
|
||||
|
||||
/// Returns whether git diff features should be shown.
|
||||
/// This includes gutter diff indicators and scrollbar diff markers.
|
||||
pub fn is_diff_enabled(&self) -> bool {
|
||||
self.enabled.diff
|
||||
}
|
||||
|
||||
/// Returns whether the git gutter should be shown.
|
||||
/// This checks both the global diff setting and the gutter-specific setting.
|
||||
pub fn is_gutter_enabled(&self) -> bool {
|
||||
self.is_diff_enabled()
|
||||
&& matches!(self.git_gutter, settings::GitGutterSetting::TrackedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Whether git integration is enabled for showing git status.
|
||||
///
|
||||
/// Default: true
|
||||
pub status: bool,
|
||||
/// Whether git integration is enabled for showing diffs.
|
||||
///
|
||||
/// Default: true
|
||||
pub diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default)]
|
||||
pub enum GitPathStyle {
|
||||
#[default]
|
||||
@@ -502,7 +539,14 @@ impl Settings for ProjectSettings {
|
||||
let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
|
||||
|
||||
let git = content.git.as_ref().unwrap();
|
||||
let git_enabled = {
|
||||
GitEnabledSettings {
|
||||
status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
|
||||
diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
|
||||
}
|
||||
};
|
||||
let git_settings = GitSettings {
|
||||
enabled: git_enabled,
|
||||
git_gutter: git.git_gutter.unwrap(),
|
||||
gutter_debounce: git.gutter_debounce.unwrap_or_default(),
|
||||
inline_blame: {
|
||||
|
||||
@@ -92,7 +92,13 @@ impl Settings for ProjectPanelSettings {
|
||||
entry_spacing: project_panel.entry_spacing.unwrap(),
|
||||
file_icons: project_panel.file_icons.unwrap(),
|
||||
folder_icons: project_panel.folder_icons.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap(),
|
||||
git_status: project_panel.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
indent_size: project_panel.indent_size.unwrap(),
|
||||
indent_guides: IndentGuidesSettings {
|
||||
show: project_panel.indent_guides.unwrap().show.unwrap(),
|
||||
|
||||
@@ -28,6 +28,11 @@ parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod prompts;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use strum::{EnumIter, IntoEnumIterator as _};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
@@ -51,11 +52,51 @@ pub struct PromptMetadata {
|
||||
pub saved_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl PromptMetadata {
|
||||
fn builtin(builtin: BuiltInPrompt) -> Self {
|
||||
Self {
|
||||
id: PromptId::BuiltIn(builtin),
|
||||
title: Some(builtin.title().into()),
|
||||
default: false,
|
||||
saved_at: DateTime::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in prompts that have default content and can be customized by users.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
|
||||
pub enum BuiltInPrompt {
|
||||
CommitMessage,
|
||||
}
|
||||
|
||||
impl BuiltInPrompt {
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => "Commit message",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default content for this built-in prompt.
|
||||
pub fn default_content(&self) -> &'static str {
|
||||
match self {
|
||||
Self::CommitMessage => include_str!("../../git_ui/src/commit_message_prompt.txt"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BuiltInPrompt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::CommitMessage => write!(f, "Commit message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum PromptId {
|
||||
User { uuid: UserPromptId },
|
||||
CommitMessage,
|
||||
BuiltIn(BuiltInPrompt),
|
||||
}
|
||||
|
||||
impl PromptId {
|
||||
@@ -63,31 +104,37 @@ impl PromptId {
|
||||
UserPromptId::new().into()
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> Option<UserPromptId> {
|
||||
pub fn as_user(&self) -> Option<UserPromptId> {
|
||||
match self {
|
||||
Self::User { uuid } => Some(*uuid),
|
||||
_ => None,
|
||||
Self::BuiltIn { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_built_in(&self) -> Option<BuiltInPrompt> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::BuiltIn(builtin) => Some(*builtin),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_built_in(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } => false,
|
||||
Self::CommitMessage => true,
|
||||
}
|
||||
matches!(self, Self::BuiltIn { .. })
|
||||
}
|
||||
|
||||
pub fn can_edit(&self) -> bool {
|
||||
match self {
|
||||
Self::User { .. } | Self::CommitMessage => true,
|
||||
Self::User { .. } => true,
|
||||
Self::BuiltIn(builtin) => match builtin {
|
||||
BuiltInPrompt::CommitMessage => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_content(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::User { .. } => None,
|
||||
Self::CommitMessage => Some(include_str!("../../git_ui/src/commit_message_prompt.txt")),
|
||||
}
|
||||
impl From<BuiltInPrompt> for PromptId {
|
||||
fn from(builtin: BuiltInPrompt) -> Self {
|
||||
PromptId::BuiltIn(builtin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +164,7 @@ impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PromptId::User { uuid } => write!(f, "{}", uuid.0),
|
||||
PromptId::CommitMessage => write!(f, "Commit message"),
|
||||
PromptId::BuiltIn(builtin) => write!(f, "{}", builtin),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +197,16 @@ impl MetadataCache {
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(prompt_id, metadata);
|
||||
}
|
||||
|
||||
// Insert all the built-in prompts that were not customized by the user
|
||||
for builtin in BuiltInPrompt::iter() {
|
||||
let builtin_id = PromptId::BuiltIn(builtin);
|
||||
if !cache.metadata_by_id.contains_key(&builtin_id) {
|
||||
let metadata = PromptMetadata::builtin(builtin);
|
||||
cache.metadata.push(metadata.clone());
|
||||
cache.metadata_by_id.insert(builtin_id, metadata);
|
||||
}
|
||||
}
|
||||
cache.sort();
|
||||
Ok(cache)
|
||||
}
|
||||
@@ -198,10 +255,6 @@ impl PromptStore {
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
|
||||
metadata.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
bodies.delete(&mut txn, &PromptId::CommitMessage)?;
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
@@ -294,7 +347,16 @@ impl PromptStore {
|
||||
let bodies = self.bodies;
|
||||
cx.background_spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into();
|
||||
let mut prompt: String = match bodies.get(&txn, &id)? {
|
||||
Some(body) => body.into(),
|
||||
None => {
|
||||
if let Some(built_in) = id.as_built_in() {
|
||||
built_in.default_content().into()
|
||||
} else {
|
||||
anyhow::bail!("prompt not found")
|
||||
}
|
||||
}
|
||||
};
|
||||
LineEnding::normalize(&mut prompt);
|
||||
Ok(prompt)
|
||||
})
|
||||
@@ -339,11 +401,6 @@ impl PromptStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the number of prompts in the store.
|
||||
pub fn prompt_count(&self) -> usize {
|
||||
self.metadata_cache.read().metadata.len()
|
||||
}
|
||||
|
||||
pub fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
|
||||
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
|
||||
}
|
||||
@@ -412,23 +469,38 @@ impl PromptStore {
|
||||
return Task::ready(Err(anyhow!("this prompt cannot be edited")));
|
||||
}
|
||||
|
||||
let prompt_metadata = PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
let body = body.to_string();
|
||||
let is_default_content = id
|
||||
.as_built_in()
|
||||
.is_some_and(|builtin| body.trim() == builtin.default_content().trim());
|
||||
|
||||
let metadata = if let Some(builtin) = id.as_built_in() {
|
||||
PromptMetadata::builtin(builtin)
|
||||
} else {
|
||||
PromptMetadata {
|
||||
id,
|
||||
title,
|
||||
default,
|
||||
saved_at: Utc::now(),
|
||||
}
|
||||
};
|
||||
self.metadata_cache.write().insert(prompt_metadata.clone());
|
||||
|
||||
self.metadata_cache.write().insert(metadata.clone());
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
let metadata_db = self.metadata;
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
bodies.put(&mut txn, &id, &body.to_string())?;
|
||||
if is_default_content {
|
||||
metadata_db.delete(&mut txn, &id)?;
|
||||
bodies.delete(&mut txn, &id)?;
|
||||
} else {
|
||||
metadata_db.put(&mut txn, &id, &metadata)?;
|
||||
bodies.put(&mut txn, &id, &body)?;
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
@@ -490,3 +562,122 @@ impl PromptStore {
|
||||
pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_built_in_prompt_load_save(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let db_path = temp_dir.path().join("prompts-db");
|
||||
|
||||
let store = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
|
||||
let store = cx.new(|_cx| store);
|
||||
|
||||
let commit_message_id = PromptId::BuiltIn(BuiltInPrompt::CommitMessage);
|
||||
|
||||
let loaded_content = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut expected_content = BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content);
|
||||
assert_eq!(
|
||||
loaded_content.trim(),
|
||||
expected_content.trim(),
|
||||
"Loading a built-in prompt not in DB should return default content"
|
||||
);
|
||||
|
||||
let metadata = store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata.is_some(),
|
||||
"Built-in prompt should always have metadata"
|
||||
);
|
||||
assert!(
|
||||
store.read_with(cx, |store, _| {
|
||||
store
|
||||
.metadata_cache
|
||||
.read()
|
||||
.metadata_by_id
|
||||
.contains_key(&commit_message_id)
|
||||
}),
|
||||
"Built-in prompt should always be in cache"
|
||||
);
|
||||
|
||||
let custom_content = "Custom commit message prompt";
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(custom_content),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let loaded_custom = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
loaded_custom.trim(),
|
||||
custom_content.trim(),
|
||||
"Custom content should be loaded after saving"
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.read_with(cx, |store, _| store.metadata(commit_message_id))
|
||||
.is_some(),
|
||||
"Built-in prompt should have metadata after customization"
|
||||
);
|
||||
|
||||
store
|
||||
.update(cx, |store, cx| {
|
||||
store.save(
|
||||
commit_message_id,
|
||||
Some("Commit message".into()),
|
||||
false,
|
||||
Rope::from(BuiltInPrompt::CommitMessage.default_content()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let metadata_after_reset =
|
||||
store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
||||
assert!(
|
||||
metadata_after_reset.is_some(),
|
||||
"Built-in prompt should still have metadata after reset"
|
||||
);
|
||||
assert_eq!(
|
||||
metadata_after_reset
|
||||
.as_ref()
|
||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
||||
Some("Commit message"),
|
||||
"Built-in prompt should have default title after reset"
|
||||
);
|
||||
|
||||
let loaded_after_reset = store
|
||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut expected_content_after_reset =
|
||||
BuiltInPrompt::CommitMessage.default_content().to_string();
|
||||
LineEnding::normalize(&mut expected_content_after_reset);
|
||||
assert_eq!(
|
||||
loaded_after_reset.trim(),
|
||||
expected_content_after_reset.trim(),
|
||||
"After saving default content, load should return default"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
|
||||
use editor::{CompletionProvider, SelectionEffects};
|
||||
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
|
||||
use gpui::{
|
||||
Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable,
|
||||
PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle,
|
||||
WindowOptions, actions, point, size, transparent_black,
|
||||
App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
|
||||
Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions,
|
||||
actions, point, size, transparent_black,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
|
||||
use language_model::{
|
||||
@@ -21,7 +21,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use title_bar::platform_title_bar::PlatformTitleBar;
|
||||
use ui::{Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
|
||||
use zed_actions::assistant::InlineAssist;
|
||||
@@ -206,13 +206,8 @@ impl PickerDelegate for RulePickerDelegate {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.read(cx).prompt_count() == 0 {
|
||||
"No rules.".into()
|
||||
} else {
|
||||
"No rules found matching your search.".into()
|
||||
};
|
||||
Some(text)
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
Some("No rules found matching your search.".into())
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -680,13 +675,13 @@ impl RulesLibrary {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(default_content) = prompt_id.default_content() else {
|
||||
let Some(built_in) = prompt_id.as_built_in() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
|
||||
rule_editor.body_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(default_content, window, cx);
|
||||
editor.set_text(built_in.default_content(), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1428,31 +1423,7 @@ impl Render for RulesLibrary {
|
||||
this.border_t_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(self.render_rule_list(cx))
|
||||
.map(|el| {
|
||||
if self.store.read(cx).prompt_count() == 0 {
|
||||
el.child(
|
||||
v_flex()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
Button::new("create-rule", "New Rule")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.key_binding(KeyBinding::for_action(&NewRule, cx))
|
||||
.on_click(|_, window, cx| {
|
||||
window
|
||||
.dispatch_action(NewRule.boxed_clone(), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
el.child(self.render_active_rule(cx))
|
||||
}
|
||||
}),
|
||||
.child(self.render_active_rule(cx)),
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -288,6 +288,11 @@ impl std::fmt::Debug for ContextServerCommand {
|
||||
#[with_fallible_options]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not to enable git integration.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(flatten)]
|
||||
pub enabled: Option<GitEnabledSettings>,
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
@@ -317,6 +322,41 @@ pub struct GitSettings {
|
||||
pub path_style: Option<GitPathStyle>,
|
||||
}
|
||||
|
||||
/// Global controls for enabling or disabling git integration features.
|
||||
///
|
||||
/// These settings provide a centralized way to control git integration,
|
||||
/// rather than having to configure each feature individually.
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct GitEnabledSettings {
|
||||
/// Master switch to disable all git integration features.
|
||||
/// When true, all git features are disabled regardless of other settings.
|
||||
///
|
||||
/// Default: false
|
||||
pub disable_git: Option<bool>,
|
||||
/// Whether to show git status indicators (modified, added, deleted)
|
||||
/// in the project panel, outline panel, and tabs.
|
||||
///
|
||||
/// Default: true
|
||||
pub enable_status: Option<bool>,
|
||||
/// Whether to show git diff information, including gutter diff
|
||||
/// indicators and scrollbar diff markers.
|
||||
///
|
||||
/// Default: true
|
||||
pub enable_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl GitEnabledSettings {
|
||||
pub fn is_git_status_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_status.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn is_git_diff_enabled(&self) -> bool {
|
||||
!self.disable_git.unwrap_or(false) && self.enable_diff.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
||||
@@ -5519,6 +5519,102 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
SettingsPage {
|
||||
title: "Version Control",
|
||||
items: vec![
|
||||
SettingsPageItem::SectionHeader("Git Integration"),
|
||||
SettingsPageItem::DynamicItem(DynamicItem {
|
||||
discriminant: SettingItem {
|
||||
files: USER,
|
||||
title: "Disable Git Integration",
|
||||
description: "Disable all Git integration features in Zed.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.disable_git"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.disable_git = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
pick_discriminant: |settings_content| {
|
||||
let disabled = settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.disable_git
|
||||
.unwrap_or(false);
|
||||
Some(if disabled { 0 } else { 1 })
|
||||
},
|
||||
fields: vec![
|
||||
vec![],
|
||||
vec![
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Status",
|
||||
description: "Show Git status information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_status"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_status
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_status = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
SettingItem {
|
||||
files: USER,
|
||||
title: "Enable Git Diff",
|
||||
description: "Show Git diff information in the editor.",
|
||||
field: Box::new(SettingField::<bool> {
|
||||
json_path: Some("git.enable_diff"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()?
|
||||
.enable_diff
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.enabled
|
||||
.get_or_insert_default()
|
||||
.enable_diff = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Git Gutter"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Visibility",
|
||||
|
||||
@@ -790,8 +790,7 @@ impl TerminalPanel {
|
||||
}
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = pane.has_focus(window, cx)
|
||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
||||
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||
});
|
||||
|
||||
@@ -853,8 +852,7 @@ impl TerminalPanel {
|
||||
}
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = pane.has_focus(window, cx)
|
||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
||||
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||
});
|
||||
|
||||
@@ -1171,64 +1169,67 @@ pub fn new_terminal_pane(
|
||||
let source = tab.pane.clone();
|
||||
let item_id_to_move = item.item_id();
|
||||
|
||||
let Ok(new_split_pane) = pane
|
||||
.drag_split_direction()
|
||||
.map(|split_direction| {
|
||||
drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel.center.split(
|
||||
&this_pane,
|
||||
&new_pane,
|
||||
split_direction,
|
||||
cx,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
else {
|
||||
return ControlFlow::Break(());
|
||||
// If no split direction, let the regular pane drop handler take care of it
|
||||
let Some(split_direction) = pane.drag_split_direction() else {
|
||||
return ControlFlow::Continue(());
|
||||
};
|
||||
|
||||
match new_split_pane.transpose() {
|
||||
// Source pane may be the one currently updated, so defer the move.
|
||||
Ok(Some(new_pane)) => cx
|
||||
.spawn_in(window, async move |_, cx| {
|
||||
cx.update(|window, cx| {
|
||||
move_item(
|
||||
&source,
|
||||
&new_pane,
|
||||
item_id_to_move,
|
||||
new_pane.read(cx).active_item_index(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
// Gather data synchronously before deferring
|
||||
let is_zoomed = drop_closure_terminal_panel
|
||||
.upgrade()
|
||||
.map(|terminal_panel| {
|
||||
let terminal_panel = terminal_panel.read(cx);
|
||||
if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
}
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let workspace = workspace.clone();
|
||||
let terminal_panel = drop_closure_terminal_panel.clone();
|
||||
|
||||
// Defer the split operation to avoid re-entrancy panic.
|
||||
// The pane may be the one currently being updated, so we cannot
|
||||
// call mark_positions (via split) synchronously.
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
cx.update(|window, cx| {
|
||||
let Ok(new_pane) =
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace, project, is_zoomed, window, cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel.center.split(
|
||||
&this_pane,
|
||||
&new_pane,
|
||||
split_direction,
|
||||
cx,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach(),
|
||||
// If we drop into existing pane or current pane,
|
||||
// regular pane drop handler will take care of it,
|
||||
// using the right tab index for the operation.
|
||||
Ok(None) => return ControlFlow::Continue(()),
|
||||
err @ Err(_) => {
|
||||
err.log_err();
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
};
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(new_pane) = new_pane.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
move_item(
|
||||
&source,
|
||||
&new_pane,
|
||||
item_id_to_move,
|
||||
new_pane.read(cx).active_item_index(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
} else if let Some(project_path) = item.project_path(cx)
|
||||
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
|
||||
Severity::Info => (
|
||||
IconName::Info,
|
||||
Color::Muted,
|
||||
cx.theme().colors().panel_background.opacity(0.),
|
||||
cx.theme().status().info_background.opacity(0.1),
|
||||
),
|
||||
Severity::Success => (
|
||||
IconName::Check,
|
||||
|
||||
@@ -76,7 +76,13 @@ impl Settings for ItemSettings {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let tabs = content.tabs.as_ref().unwrap();
|
||||
Self {
|
||||
git_status: tabs.git_status.unwrap(),
|
||||
git_status: tabs.git_status.unwrap()
|
||||
&& content
|
||||
.git
|
||||
.unwrap()
|
||||
.enabled
|
||||
.unwrap()
|
||||
.is_git_status_enabled(),
|
||||
close_position: tabs.close_position.unwrap(),
|
||||
activate_on_close: tabs.activate_on_close.unwrap(),
|
||||
file_icons: tabs.file_icons.unwrap(),
|
||||
|
||||
@@ -193,6 +193,12 @@ impl Render for ModalLayer {
|
||||
background.fade_out(0.2);
|
||||
this.bg(background)
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.hide_modal(window, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(px(0.0))
|
||||
|
||||
@@ -3296,4 +3296,53 @@ mod tests {
|
||||
|
||||
assert_eq!(workspace.center_group, new_workspace.center_group);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_workspace_window_bounds() {
|
||||
zlog::init_test();
|
||||
|
||||
let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
|
||||
let id = db.next_id().await.unwrap();
|
||||
|
||||
// Create a workspace with empty paths (empty workspace)
|
||||
let empty_paths: &[&str] = &[];
|
||||
let display_uuid = Uuid::new_v4();
|
||||
let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
|
||||
origin: point(px(100.0), px(200.0)),
|
||||
size: size(px(800.0), px(600.0)),
|
||||
}));
|
||||
|
||||
let workspace = SerializedWorkspace {
|
||||
id,
|
||||
paths: PathList::new(empty_paths),
|
||||
location: SerializedWorkspaceLocation::Local,
|
||||
center_group: Default::default(),
|
||||
window_bounds: None,
|
||||
display: None,
|
||||
docks: Default::default(),
|
||||
breakpoints: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
window_id: None,
|
||||
user_toolchains: Default::default(),
|
||||
};
|
||||
|
||||
// Save the workspace (this creates the record with empty paths)
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
// Save window bounds separately (as the actual code does via set_window_open_status)
|
||||
db.set_window_open_status(id, window_bounds, display_uuid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Retrieve it using empty paths
|
||||
let retrieved = db.workspace_for_roots(empty_paths).unwrap();
|
||||
|
||||
// Verify window bounds were persisted
|
||||
assert_eq!(retrieved.id, id);
|
||||
assert!(retrieved.window_bounds.is_some());
|
||||
assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
|
||||
assert!(retrieved.display.is_some());
|
||||
assert_eq!(retrieved.display.unwrap(), display_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1748,26 +1748,18 @@ impl Workspace {
|
||||
window
|
||||
} else {
|
||||
let window_bounds_override = window_bounds_env_override();
|
||||
let is_empty_workspace = project_paths.is_empty();
|
||||
|
||||
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||
(Some(WindowBounds::Windowed(bounds)), None)
|
||||
} else if let Some(workspace) = serialized_workspace.as_ref() {
|
||||
} else if let Some(workspace) = serialized_workspace.as_ref()
|
||||
&& let Some(display) = workspace.display
|
||||
&& let Some(bounds) = workspace.window_bounds.as_ref()
|
||||
{
|
||||
// Reopening an existing workspace - restore its saved bounds
|
||||
if let (Some(display), Some(bounds)) =
|
||||
(workspace.display, workspace.window_bounds.as_ref())
|
||||
{
|
||||
(Some(bounds.0), Some(display))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else if is_empty_workspace {
|
||||
// Empty workspace - try to restore the last known no-project window bounds
|
||||
if let Some((display, bounds)) = persistence::read_default_window_bounds() {
|
||||
(Some(bounds), Some(display))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
(Some(bounds.0), Some(display))
|
||||
} else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
|
||||
// New or empty workspace - use the last known window bounds
|
||||
(Some(bounds), Some(display))
|
||||
} else {
|
||||
// New window - let GPUI's default_bounds() handle cascading
|
||||
(None, None)
|
||||
@@ -5673,12 +5665,24 @@ impl Workspace {
|
||||
persistence::DB.save_workspace(serialized_workspace).await;
|
||||
})
|
||||
}
|
||||
WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
|
||||
persistence::DB
|
||||
.set_session_id(database_id, None)
|
||||
.await
|
||||
.log_err();
|
||||
}),
|
||||
WorkspaceLocation::DetachFromSession => {
|
||||
let window_bounds = SerializedWindowBounds(window.window_bounds());
|
||||
let display = window.display(cx).and_then(|d| d.uuid().ok());
|
||||
window.spawn(cx, async move |_| {
|
||||
persistence::DB
|
||||
.set_window_open_status(
|
||||
database_id,
|
||||
window_bounds,
|
||||
display.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
persistence::DB
|
||||
.set_session_id(database_id, None)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
WorkspaceLocation::None => Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +354,8 @@ pub mod agent {
|
||||
ResetAgentZoom,
|
||||
/// Toggles the utility/agent pane open/closed state.
|
||||
ToggleAgentPane,
|
||||
/// Pastes clipboard content without any formatting.
|
||||
PasteRaw,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet
|
||||
|
||||
You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
|
||||
|
||||
> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
|
||||
> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s >
|
||||
> **Input Sources** and uncheck **Select the previous input source**.
|
||||
|
||||
For more information, see:
|
||||
|
||||
- [Configuring Supported Languages](./configuring-languages.md)
|
||||
|
||||
@@ -73,7 +73,7 @@ h_flex()
|
||||
|
||||
- `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering.
|
||||
- `Modal`: A UI element that floats on top of the rest of the UI
|
||||
- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.)
|
||||
- `Picker`: A struct representing a list of items floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Modal' in the image below is a picker.)
|
||||
- `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate.
|
||||
- `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below).
|
||||
- `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below).
|
||||
|
||||
@@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
&should_run_tests,
|
||||
]);
|
||||
|
||||
let check_style = check_style();
|
||||
let run_tests_linux = run_platform_tests(Platform::Linux);
|
||||
let call_autofix = call_autofix(&check_style, &run_tests_linux);
|
||||
|
||||
let mut jobs = vec![
|
||||
orchestrate,
|
||||
check_style(),
|
||||
check_style,
|
||||
should_run_tests.guard(run_platform_tests(Platform::Windows)),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Linux)),
|
||||
should_run_tests.guard(run_tests_linux),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
||||
should_run_tests.guard(doctests()),
|
||||
should_run_tests.guard(check_workspace_binaries()),
|
||||
@@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow {
|
||||
workflow
|
||||
})
|
||||
.add_job(tests_pass.name, tests_pass.job)
|
||||
.add_job(call_autofix.name, call_autofix.job)
|
||||
}
|
||||
|
||||
// Generates a bash script that checks changed files against regex patterns
|
||||
@@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
|
||||
|
||||
fn check_style() -> NamedJob {
|
||||
fn check_for_typos() -> Step<Use> {
|
||||
named::uses(
|
||||
@@ -236,15 +243,58 @@ fn check_style() -> NamedJob {
|
||||
.add_step(steps::checkout_repo())
|
||||
.add_step(steps::cache_rust_dependencies_namespace())
|
||||
.add_step(steps::setup_pnpm())
|
||||
.add_step(steps::script("./script/prettier"))
|
||||
.add_step(steps::prettier())
|
||||
.add_step(steps::cargo_fmt())
|
||||
.add_step(steps::trigger_autofix(false))
|
||||
.add_step(steps::record_style_failure())
|
||||
.add_step(steps::script("./script/check-todos"))
|
||||
.add_step(steps::script("./script/check-keymaps"))
|
||||
.add_step(check_for_typos()),
|
||||
.add_step(check_for_typos())
|
||||
.outputs([(
|
||||
STYLE_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_STYLE_FAILURE_STEP_ID
|
||||
),
|
||||
)]),
|
||||
)
|
||||
}
|
||||
|
||||
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
|
||||
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
|
||||
let clippy_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
|
||||
);
|
||||
named::bash(format!(
|
||||
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
|
||||
clippy_failed_expr
|
||||
))
|
||||
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
|
||||
}
|
||||
|
||||
let style_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
check_style.name, STYLE_FAILED_OUTPUT
|
||||
);
|
||||
let clippy_failed_expr = format!(
|
||||
"needs.{}.outputs.{} == 'true'",
|
||||
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
|
||||
);
|
||||
let (authenticate, _token) = steps::authenticate_as_zippy();
|
||||
|
||||
let job = Job::default()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(format!(
|
||||
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
|
||||
style_failed_expr, clippy_failed_expr
|
||||
)))
|
||||
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
|
||||
.add_step(authenticate)
|
||||
.add_step(dispatch_autofix(&run_tests_linux.name));
|
||||
|
||||
named::job(job)
|
||||
}
|
||||
|
||||
fn check_dependencies() -> NamedJob {
|
||||
fn install_cargo_machete() -> Step<Use> {
|
||||
named::uses(
|
||||
@@ -305,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob {
|
||||
)
|
||||
}
|
||||
|
||||
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
|
||||
|
||||
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
||||
let runner = match platform {
|
||||
Platform::Windows => runners::WINDOWS_DEFAULT,
|
||||
@@ -327,12 +379,23 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
||||
.add_step(steps::setup_node())
|
||||
.add_step(steps::clippy(platform))
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.add_step(steps::trigger_autofix(true))
|
||||
.add_step(steps::cargo_install_nextest())
|
||||
job.add_step(steps::record_clippy_failure())
|
||||
})
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.add_step(steps::cargo_install_nextest())
|
||||
})
|
||||
.add_step(steps::clear_target_dir_if_large(platform))
|
||||
.add_step(steps::cargo_nextest(platform))
|
||||
.add_step(steps::cleanup_cargo_config(platform)),
|
||||
.add_step(steps::cleanup_cargo_config(platform))
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.outputs([(
|
||||
CLIPPY_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_CLIPPY_FAILURE_STEP_ID
|
||||
),
|
||||
)])
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step<Use> {
|
||||
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
|
||||
}
|
||||
|
||||
pub const PRETTIER_STEP_ID: &str = "prettier";
|
||||
pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
|
||||
pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
|
||||
|
||||
pub fn prettier() -> Step<Run> {
|
||||
named::bash("./script/prettier").id(PRETTIER_STEP_ID)
|
||||
}
|
||||
|
||||
pub fn cargo_fmt() -> Step<Run> {
|
||||
named::bash("cargo fmt --all -- --check")
|
||||
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
|
||||
}
|
||||
|
||||
pub fn record_style_failure() -> Step<Run> {
|
||||
named::bash(format!(
|
||||
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
|
||||
))
|
||||
.id(RECORD_STYLE_FAILURE_STEP_ID)
|
||||
.if_condition(Expression::new("always()"))
|
||||
}
|
||||
|
||||
pub fn cargo_install_nextest() -> Step<Use> {
|
||||
@@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
|
||||
}
|
||||
}
|
||||
|
||||
pub const CLIPPY_STEP_ID: &str = "clippy";
|
||||
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
|
||||
|
||||
pub fn clippy(platform: Platform) -> Step<Run> {
|
||||
match platform {
|
||||
Platform::Windows => named::pwsh("./script/clippy.ps1"),
|
||||
_ => named::bash("./script/clippy"),
|
||||
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
|
||||
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_clippy_failure() -> Step<Run> {
|
||||
named::bash(format!(
|
||||
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||
CLIPPY_STEP_ID
|
||||
))
|
||||
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
|
||||
.if_condition(Expression::new("always()"))
|
||||
}
|
||||
|
||||
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
|
||||
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
|
||||
}
|
||||
@@ -345,16 +374,6 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
|
||||
named::bash(format!(
|
||||
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}"
|
||||
))
|
||||
.if_condition(Expression::new(
|
||||
"failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
|
||||
))
|
||||
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
|
||||
}
|
||||
|
||||
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
||||
let step = named::uses(
|
||||
"actions",
|
||||
|
||||
Reference in New Issue
Block a user