Compare commits
1 Commits
ex-pointer
...
fix-vim-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ae9d6e1f |
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,8 +61,7 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- id: cargo_fmt
|
||||
name: steps::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,8 +26,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -72,15 +71,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
- 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
|
||||
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
|
||||
@@ -94,8 +93,6 @@ 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')
|
||||
@@ -114,8 +111,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::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,8 +20,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -45,8 +44,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::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,19 +74,18 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
- name: ./script/prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- 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"
|
||||
- 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
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -97,8 +96,6 @@ 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:
|
||||
@@ -119,8 +116,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -167,15 +163,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
- 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
|
||||
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
|
||||
@@ -189,8 +185,6 @@ 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:
|
||||
@@ -211,8 +205,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -592,24 +585,6 @@ 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
|
||||
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.2"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -243,9 +243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -793,7 +793,7 @@ dependencies = [
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -7370,7 +7370,7 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.3",
|
||||
@@ -18927,6 +18927,18 @@ 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"
|
||||
@@ -18941,14 +18953,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.9"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -18961,7 +18973,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +293,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,7 +304,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -267,7 +267,6 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -336,7 +335,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -349,7 +347,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -297,7 +296,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,7 +308,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -192,7 +192,6 @@ 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>,
|
||||
}
|
||||
|
||||
@@ -223,11 +222,6 @@ 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
|
||||
@@ -238,7 +232,6 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -314,7 +307,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1363,7 +1355,6 @@ 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);
|
||||
|
||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -543,9 +543,6 @@ 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())
|
||||
@@ -556,127 +553,133 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// 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()?;
|
||||
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 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)
|
||||
});
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -687,13 +690,6 @@ 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>,
|
||||
@@ -971,7 +967,6 @@ 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() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
Arc::new(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: &HashSet<ModelId>,
|
||||
favorites: Arc<HashSet<ModelId>>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -572,11 +572,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
||||
Arc::new(
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
@@ -607,7 +609,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(),
|
||||
@@ -623,7 +625,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(),
|
||||
@@ -639,7 +641,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 {
|
||||
@@ -660,7 +662,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");
|
||||
@@ -681,7 +683,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!(
|
||||
@@ -721,7 +723,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::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||
@@ -260,7 +260,6 @@ impl ThreadFeedbackState {
|
||||
|
||||
pub struct AcpThreadView {
|
||||
agent: Rc<dyn AgentServer>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
@@ -407,7 +406,6 @@ impl AcpThreadView {
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
agent_server_store,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
@@ -739,7 +737,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, {
|
||||
@@ -781,6 +779,7 @@ impl AcpThreadView {
|
||||
configuration_view,
|
||||
description: err
|
||||
.description
|
||||
.clone()
|
||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||
_subscription: subscription,
|
||||
};
|
||||
@@ -1089,7 +1088,10 @@ impl AcpThreadView {
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
@@ -1661,6 +1663,44 @@ 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()
|
||||
@@ -2113,7 +2153,6 @@ 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);
|
||||
@@ -2123,55 +2162,36 @@ impl AcpThreadView {
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
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(),
|
||||
)
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
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(),
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let has_terminals = tool_call.terminals().next().is_some();
|
||||
@@ -2203,7 +2223,7 @@ impl AcpThreadView {
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.pl_5()
|
||||
.pl(rems_from_px(20.0))
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
@@ -2420,12 +2440,6 @@ 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 {
|
||||
@@ -2467,25 +2481,7 @@ impl AcpThreadView {
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Canceled => v_flex()
|
||||
.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())),
|
||||
)
|
||||
})
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
@@ -2584,7 +2580,7 @@ impl AcpThreadView {
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
@@ -2770,20 +2766,20 @@ impl AcpThreadView {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.gap_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()
|
||||
.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()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
}
|
||||
})
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
@@ -3504,119 +3500,138 @@ impl AcpThreadView {
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> Div {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
.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
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
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(),
|
||||
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)),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
.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))
|
||||
})
|
||||
.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())
|
||||
};
|
||||
|
||||
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()
|
||||
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::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
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
@@ -5865,6 +5880,10 @@ 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| {
|
||||
@@ -5873,14 +5892,7 @@ impl AcpThreadView {
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -5893,10 +5905,14 @@ 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, AuthRequired::new(), agent, connection, window, cx);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5999,19 +6015,16 @@ impl Render for AcpThreadView {
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.child(self.render_auth_required_state(
|
||||
} => self
|
||||
.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
ThreadState::Loading { .. } => v_flex()
|
||||
.flex_1()
|
||||
.child(self.render_recent_history(cx))
|
||||
|
||||
@@ -71,7 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
@@ -1698,9 +1698,6 @@ 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())
|
||||
@@ -1711,101 +1708,84 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// 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()?;
|
||||
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 line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
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();
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
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();
|
||||
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 text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
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```");
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
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);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
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 crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
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);
|
||||
|
||||
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;
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1964,12 +1944,6 @@ 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);
|
||||
@@ -2653,7 +2627,6 @@ 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,9 +103,8 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
// 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::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
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, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,19 +300,14 @@ 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");
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
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
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
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, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,19 +272,14 @@ 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");
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
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
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -348,61 +348,6 @@ 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| {
|
||||
|
||||
@@ -20880,36 +20880,6 @@ 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
|
||||
@@ -20949,66 +20919,6 @@ 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,
|
||||
|
||||
@@ -331,6 +331,7 @@ 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",
|
||||
@@ -523,12 +524,6 @@ 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
|
||||
@@ -545,12 +540,6 @@ 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
|
||||
|
||||
@@ -15,7 +15,6 @@ use askpass::AskPassDelegate;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::RewrapOptions;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
|
||||
actions::ExpandAllDiffHunks,
|
||||
@@ -2181,13 +2180,7 @@ impl GitPanel {
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
|
||||
let wrapped_message = editor.update(cx, |editor, cx| {
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.rewrap_impl(
|
||||
RewrapOptions {
|
||||
override_language_settings: false,
|
||||
preserve_existing_whitespace: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.rewrap(&Default::default(), window, cx);
|
||||
editor.text(cx)
|
||||
});
|
||||
if wrapped_message.trim().is_empty() {
|
||||
|
||||
@@ -566,22 +566,22 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Gemini25FlashLite
|
||||
| Self::Gemini25Flash
|
||||
| Self::Gemini25Pro
|
||||
| Self::Gemini3Pro
|
||||
| Self::Gemini3Flash => 1_048_576,
|
||||
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::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Model::Gemini25FlashLite
|
||||
| Model::Gemini25Flash
|
||||
| Model::Gemini25Pro
|
||||
| Model::Gemini3Pro
|
||||
| Model::Gemini3Flash => Some(65_536),
|
||||
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::Custom { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
wayland-client = { version = "0.31.2", optional = true }
|
||||
wayland-cursor = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.31.2", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 {
|
||||
@@ -24,10 +23,7 @@ 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, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
|
||||
|
||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -56,28 +52,8 @@ 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();
|
||||
})),
|
||||
@@ -110,7 +86,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -126,39 +101,6 @@ 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,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -174,7 +116,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: true,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -190,7 +131,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -207,7 +147,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -223,7 +162,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -239,7 +177,6 @@ impl Render for WindowDemo {
|
||||
|_, cx| {
|
||||
cx.new(|_| SubWindow {
|
||||
custom_titlebar: false,
|
||||
is_dialog: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -28,8 +28,6 @@ pub use entity_map::*;
|
||||
use http_client::{HttpClient, Url};
|
||||
use smallvec::SmallVec;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_app::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_context::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
|
||||
@@ -53,8 +51,6 @@ mod async_context;
|
||||
mod context;
|
||||
mod entity_map;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test_app;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod test_context;
|
||||
|
||||
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
||||
|
||||
@@ -1,605 +0,0 @@
|
||||
//! A clean testing API for GPUI applications.
|
||||
//!
|
||||
//! `TestApp` provides a simpler alternative to `TestAppContext` with:
|
||||
//! - Automatic effect flushing after updates
|
||||
//! - Clean window creation and inspection
|
||||
//! - Input simulation helpers
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! #[test]
|
||||
//! fn test_my_view() {
|
||||
//! let mut app = TestApp::new();
|
||||
//!
|
||||
//! let mut window = app.open_window(|window, cx| {
|
||||
//! MyView::new(window, cx)
|
||||
//! });
|
||||
//!
|
||||
//! window.update(|view, window, cx| {
|
||||
//! view.do_something(cx);
|
||||
//! });
|
||||
//!
|
||||
//! // Check rendered state
|
||||
//! assert_eq!(window.title(), Some("Expected Title"));
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
|
||||
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
|
||||
SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, app::GpuiMode,
|
||||
};
|
||||
use rand::{SeedableRng, rngs::StdRng};
|
||||
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A test application context with a clean API.
|
||||
///
|
||||
/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
|
||||
/// each update and provides simpler window management.
|
||||
pub struct TestApp {
|
||||
app: Rc<AppCell>,
|
||||
platform: Rc<TestPlatform>,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
#[allow(dead_code)]
|
||||
dispatcher: TestDispatcher,
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
impl TestApp {
|
||||
/// Create a new test application.
|
||||
pub fn new() -> Self {
|
||||
Self::with_seed(0)
|
||||
}
|
||||
|
||||
/// Create a new test application with a specific random seed.
|
||||
pub fn with_seed(seed: u64) -> Self {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
|
||||
let mut app = App::new_app(platform.clone(), asset_source, http_client);
|
||||
app.borrow_mut().mode = GpuiMode::test();
|
||||
|
||||
Self {
|
||||
app,
|
||||
platform,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
dispatcher,
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a closure with mutable access to the App context.
|
||||
/// Automatically runs until parked after the closure completes.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let result = {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update(f)
|
||||
};
|
||||
self.run_until_parked();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run a closure with read-only access to the App context.
|
||||
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
f(&app)
|
||||
}
|
||||
|
||||
/// Create a new entity in the app.
|
||||
pub fn new_entity<T: 'static>(
|
||||
&mut self,
|
||||
build: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
self.update(|cx| cx.new(build))
|
||||
}
|
||||
|
||||
/// Update an entity.
|
||||
pub fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
f: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
self.update(|cx| entity.update(cx, f))
|
||||
}
|
||||
|
||||
/// Read an entity.
|
||||
pub fn read_entity<T: 'static, R>(
|
||||
&self,
|
||||
entity: &Entity<T>,
|
||||
f: impl FnOnce(&T, &App) -> R,
|
||||
) -> R {
|
||||
self.read(|cx| f(entity.read(cx), cx))
|
||||
}
|
||||
|
||||
/// Open a test window with the given root view.
|
||||
pub fn open_window<V: Render + 'static>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> TestWindow<V> {
|
||||
let bounds = self.read(|cx| Bounds::maximized(None, cx));
|
||||
let handle = self.update(|cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| build_view(window, cx)),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
TestWindow {
|
||||
handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a test window with specific options.
|
||||
pub fn open_window_with_options<V: Render + 'static>(
|
||||
&mut self,
|
||||
options: WindowOptions,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> TestWindow<V> {
|
||||
let handle = self.update(|cx| {
|
||||
cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
TestWindow {
|
||||
handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run pending tasks until there's nothing left to do.
|
||||
pub fn run_until_parked(&self) {
|
||||
self.background_executor.run_until_parked();
|
||||
}
|
||||
|
||||
/// Advance the simulated clock by the given duration.
|
||||
pub fn advance_clock(&self, duration: Duration) {
|
||||
self.background_executor.advance_clock(duration);
|
||||
}
|
||||
|
||||
/// Spawn a future on the foreground executor.
|
||||
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
self.foreground_executor.spawn(f(self.to_async()))
|
||||
}
|
||||
|
||||
/// Spawn a future on the background executor.
|
||||
pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
|
||||
where
|
||||
R: Send + 'static,
|
||||
{
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
/// Get an async handle to the app.
|
||||
pub fn to_async(&self) -> AsyncApp {
|
||||
AsyncApp {
|
||||
app: Rc::downgrade(&self.app),
|
||||
background_executor: self.background_executor.clone(),
|
||||
foreground_executor: self.foreground_executor.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the background executor.
|
||||
pub fn background_executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
}
|
||||
|
||||
/// Get the foreground executor.
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
&self.foreground_executor
|
||||
}
|
||||
|
||||
/// Get the text system.
|
||||
pub fn text_system(&self) -> &Arc<TextSystem> {
|
||||
&self.text_system
|
||||
}
|
||||
|
||||
/// Check if a global of the given type exists.
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
self.read(|cx| cx.has_global::<G>())
|
||||
}
|
||||
|
||||
/// Set a global value.
|
||||
pub fn set_global<G: Global>(&mut self, global: G) {
|
||||
self.update(|cx| cx.set_global(global));
|
||||
}
|
||||
|
||||
/// Read a global value.
|
||||
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
|
||||
self.read(|cx| f(cx.global(), cx))
|
||||
}
|
||||
|
||||
/// Update a global value.
|
||||
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||
self.update(|cx| cx.update_global(f))
|
||||
}
|
||||
|
||||
// Platform simulation methods
|
||||
|
||||
/// Write text to the simulated clipboard.
|
||||
pub fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_clipboard(item);
|
||||
}
|
||||
|
||||
/// Read from the simulated clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Get URLs that have been opened via `cx.open_url()`.
|
||||
pub fn opened_url(&self) -> Option<String> {
|
||||
self.platform.opened_url.borrow().clone()
|
||||
}
|
||||
|
||||
/// Check if a file path prompt is pending.
|
||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.platform.did_prompt_for_new_path()
|
||||
}
|
||||
|
||||
/// Simulate answering a path selection dialog.
|
||||
pub fn simulate_new_path_selection(
|
||||
&self,
|
||||
select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
|
||||
) {
|
||||
self.platform.simulate_new_path_selection(select);
|
||||
}
|
||||
|
||||
/// Check if a prompt dialog is pending.
|
||||
pub fn has_pending_prompt(&self) -> bool {
|
||||
self.platform.has_pending_prompt()
|
||||
}
|
||||
|
||||
/// Simulate answering a prompt dialog.
|
||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||
self.platform.simulate_prompt_answer(button);
|
||||
}
|
||||
|
||||
/// Get all open windows.
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.read(|cx| cx.windows())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestApp {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A test window with inspection and simulation capabilities.
|
||||
pub struct TestWindow<V> {
|
||||
handle: WindowHandle<V>,
|
||||
app: Rc<AppCell>,
|
||||
platform: Rc<TestPlatform>,
|
||||
background_executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl<V: 'static + Render> TestWindow<V> {
|
||||
/// Get the window handle.
|
||||
pub fn handle(&self) -> WindowHandle<V> {
|
||||
self.handle
|
||||
}
|
||||
|
||||
/// Get the root view entity.
|
||||
pub fn root(&self) -> Entity<V> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |root_view, _, _| {
|
||||
root_view.downcast::<V>().expect("root view type mismatch")
|
||||
})
|
||||
.expect("window not found")
|
||||
}
|
||||
|
||||
/// Update the root view.
|
||||
/// Automatically draws the window after the update to ensure the scene is current.
|
||||
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
|
||||
let result = {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |root_view, window, cx| {
|
||||
let view = root_view.downcast::<V>().expect("root view type mismatch");
|
||||
view.update(cx, |view, cx| f(view, window, cx))
|
||||
})
|
||||
.expect("window not found")
|
||||
};
|
||||
self.background_executor.run_until_parked();
|
||||
self.draw();
|
||||
result
|
||||
}
|
||||
|
||||
/// Read the root view.
|
||||
pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
|
||||
let app = self.app.borrow();
|
||||
let view = self
|
||||
.app
|
||||
.borrow()
|
||||
.windows
|
||||
.get(self.handle.window_id())
|
||||
.and_then(|w| w.as_ref())
|
||||
.and_then(|w| w.root.clone())
|
||||
.and_then(|r| r.downcast::<V>().ok())
|
||||
.expect("window or root view not found");
|
||||
f(view.read(&app), &app)
|
||||
}
|
||||
|
||||
/// Get the window title.
|
||||
pub fn title(&self) -> Option<String> {
|
||||
let app = self.app.borrow();
|
||||
app.read_window(&self.handle, |_, _cx| {
|
||||
// TODO: expose title through Window API
|
||||
None
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Simulate a keystroke.
|
||||
/// Automatically draws the window after the keystroke.
|
||||
pub fn simulate_keystroke(&mut self, keystroke: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke).unwrap();
|
||||
{
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
self.draw();
|
||||
}
|
||||
|
||||
/// Simulate multiple keystrokes (space-separated).
|
||||
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
|
||||
for keystroke in keystrokes.split(' ') {
|
||||
self.simulate_keystroke(keystroke);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate typing text.
|
||||
pub fn simulate_input(&mut self, input: &str) {
|
||||
for char in input.chars() {
|
||||
self.simulate_keystroke(&char.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate a mouse move.
|
||||
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
|
||||
self.simulate_event(MouseMoveEvent {
|
||||
position,
|
||||
modifiers: Default::default(),
|
||||
pressed_button: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a mouse down event.
|
||||
pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_event(MouseDownEvent {
|
||||
position,
|
||||
button,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
first_mouse: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a mouse up event.
|
||||
pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_event(MouseUpEvent {
|
||||
position,
|
||||
button,
|
||||
modifiers: Default::default(),
|
||||
click_count: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a click at the given position.
|
||||
pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
|
||||
self.simulate_mouse_down(position, button);
|
||||
self.simulate_mouse_up(position, button);
|
||||
}
|
||||
|
||||
/// Simulate a scroll event.
|
||||
pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
|
||||
self.simulate_event(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: crate::ScrollDelta::Pixels(delta),
|
||||
modifiers: Default::default(),
|
||||
touch_phase: crate::TouchPhase::Moved,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate an input event.
|
||||
/// Automatically draws the window after the event.
|
||||
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
|
||||
let platform_input = event.to_platform_input();
|
||||
{
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.dispatch_event(platform_input, cx);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
self.background_executor.run_until_parked();
|
||||
self.draw();
|
||||
}
|
||||
|
||||
/// Simulate resizing the window.
|
||||
/// Automatically draws the window after the resize.
|
||||
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
|
||||
let window_id = self.handle.window_id();
|
||||
let mut app = self.app.borrow_mut();
|
||||
if let Some(Some(window)) = app.windows.get_mut(window_id) {
|
||||
if let Some(test_window) = window.platform_window.as_test() {
|
||||
test_window.simulate_resize(size);
|
||||
}
|
||||
}
|
||||
drop(app);
|
||||
self.background_executor.run_until_parked();
|
||||
self.draw();
|
||||
}
|
||||
|
||||
/// Force a redraw of the window.
|
||||
pub fn draw(&mut self) {
|
||||
let mut app = self.app.borrow_mut();
|
||||
let any_handle: AnyWindowHandle = self.handle.into();
|
||||
app.update_window(any_handle, |_, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Get a snapshot of the rendered scene for inspection.
|
||||
/// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
|
||||
pub fn scene_snapshot(&self) -> SceneSnapshot {
|
||||
let app = self.app.borrow();
|
||||
let window = app
|
||||
.windows
|
||||
.get(self.handle.window_id())
|
||||
.and_then(|w| w.as_ref())
|
||||
.expect("window not found");
|
||||
window.rendered_frame.scene.snapshot()
|
||||
}
|
||||
|
||||
/// Get the named diagnostic quads recorded during imperative paint, without inspecting the
|
||||
/// rest of the scene snapshot.
|
||||
///
|
||||
/// This is useful for tests that want a stable, semantic view of layout/paint geometry without
|
||||
/// coupling to the low-level quad/glyph output.
|
||||
pub fn diagnostic_quads(&self) -> Vec<crate::scene::test_scene::DiagnosticQuad> {
|
||||
self.scene_snapshot().diagnostic_quads
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> Clone for TestWindow<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
handle: self.handle,
|
||||
app: self.app.clone(),
|
||||
platform: self.platform.clone(),
|
||||
background_executor: self.background_executor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{FocusHandle, Focusable, div, prelude::*};
|
||||
|
||||
struct Counter {
|
||||
count: usize,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
count: 0,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&mut self, _cx: &mut Context<Self>) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for Counter {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Counter {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(format!("Count: {}", self.count))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_usage() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
let mut window = app.open_window(Counter::new);
|
||||
|
||||
window.update(|counter, _window, cx| {
|
||||
counter.increment(cx);
|
||||
});
|
||||
|
||||
window.read(|counter, _| {
|
||||
assert_eq!(counter.count, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_entity_creation() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
let entity = app.new_entity(|cx| Counter {
|
||||
count: 42,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
|
||||
app.read_entity(&entity, |counter, _| {
|
||||
assert_eq!(counter.count, 42);
|
||||
});
|
||||
|
||||
app.update_entity(&entity, |counter, _cx| {
|
||||
counter.count += 1;
|
||||
});
|
||||
|
||||
app.read_entity(&entity, |counter, _| {
|
||||
assert_eq!(counter.count, 43);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_globals() {
|
||||
let mut app = TestApp::new();
|
||||
|
||||
struct MyGlobal(String);
|
||||
impl Global for MyGlobal {}
|
||||
|
||||
assert!(!app.has_global::<MyGlobal>());
|
||||
|
||||
app.set_global(MyGlobal("hello".into()));
|
||||
|
||||
assert!(app.has_global::<MyGlobal>());
|
||||
|
||||
app.read_global::<MyGlobal, _>(|global, _| {
|
||||
assert_eq!(global.0, "hello");
|
||||
});
|
||||
|
||||
app.update_global::<MyGlobal, _>(|global, _| {
|
||||
global.0 = "world".into();
|
||||
});
|
||||
|
||||
app.read_global::<MyGlobal, _>(|global, _| {
|
||||
assert_eq!(global.0, "world");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ use crate::{
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
||||
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestPlatformWindow,
|
||||
TestScreenCaptureSource, TextSystem, VisualContext, Window, WindowBounds, WindowHandle,
|
||||
WindowOptions, app::GpuiMode,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, app::GpuiMode,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
@@ -220,7 +220,7 @@ impl TestAppContext {
|
||||
f(&cx)
|
||||
}
|
||||
|
||||
/// Adds a new window. The Window will always be backed by a `TestPlatformWindow` which
|
||||
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
||||
/// can be retrieved with `self.test_window(handle)`
|
||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||
where
|
||||
@@ -465,8 +465,8 @@ impl TestAppContext {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Returns the `TestPlatformWindow` backing the given handle.
|
||||
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestPlatformWindow {
|
||||
/// Returns the `TestWindow` backing the given handle.
|
||||
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
||||
self.app
|
||||
.borrow_mut()
|
||||
.windows
|
||||
|
||||
@@ -808,15 +808,6 @@ impl LinearColorStop {
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Returns the solid color if this is a solid background, None otherwise.
|
||||
pub fn as_solid(&self) -> Option<Hsla> {
|
||||
if self.tag == BackgroundTag::Solid {
|
||||
Some(self.solid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Use specified color space for color interpolation.
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
|
||||
|
||||
@@ -561,7 +561,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1348,10 +1348,6 @@ 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,6 +36,12 @@ 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,
|
||||
};
|
||||
@@ -55,14 +61,6 @@ 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;
|
||||
@@ -124,7 +122,6 @@ 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,
|
||||
}
|
||||
|
||||
@@ -135,7 +132,6 @@ 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
|
||||
@@ -164,7 +160,6 @@ 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,
|
||||
}
|
||||
@@ -734,7 +729,10 @@ impl LinuxClient for WaylandClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let parent = state.keyboard_focused_window.clone();
|
||||
let parent = state
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.and_then(|w| w.toplevel());
|
||||
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
@@ -753,12 +751,7 @@ 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)
|
||||
&& (state.mouse_focused_window.is_none()
|
||||
|| state
|
||||
.mouse_focused_window
|
||||
.as_ref()
|
||||
.is_some_and(|w| !w.is_blocked()));
|
||||
let need_update = state.cursor_style != Some(style);
|
||||
|
||||
if need_update {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
@@ -1018,7 +1011,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_window(
|
||||
fn get_window(
|
||||
mut state: &mut RefMut<WaylandClientState>,
|
||||
surface_id: &ObjectId,
|
||||
) -> Option<WaylandWindowStatePtr> {
|
||||
@@ -1661,30 +1654,6 @@ 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()
|
||||
@@ -2256,27 +2225,3 @@ 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::{FxHashSet, HashMap};
|
||||
use collections::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::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||
xdg::shell::client::xdg_toplevel::XdgToplevel,
|
||||
};
|
||||
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, get_window,
|
||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
|
||||
layer_shell::LayerShellNotSupportedError, px, size,
|
||||
};
|
||||
use crate::{
|
||||
@@ -87,8 +87,6 @@ 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,
|
||||
@@ -128,7 +126,7 @@ impl WaylandSurfaceState {
|
||||
surface: &wl_surface::WlSurface,
|
||||
globals: &Globals,
|
||||
params: &WindowParams,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
parent: Option<XdgToplevel>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// For layer_shell windows, create a layer surface instead of an xdg surface
|
||||
if let WindowKind::LayerShell(options) = ¶ms.kind {
|
||||
@@ -180,28 +178,10 @@ impl WaylandSurfaceState {
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
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());
|
||||
if params.kind == WindowKind::Floating {
|
||||
toplevel.set_parent(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);
|
||||
}
|
||||
@@ -218,7 +198,6 @@ impl WaylandSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
dialog,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -227,7 +206,6 @@ 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 {
|
||||
@@ -280,13 +258,7 @@ 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();
|
||||
@@ -316,7 +288,6 @@ impl WaylandWindowState {
|
||||
globals: Globals,
|
||||
gpu_context: &BladeContext,
|
||||
options: WindowParams,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let renderer = {
|
||||
let raw_window = RawWindow {
|
||||
@@ -348,8 +319,6 @@ impl WaylandWindowState {
|
||||
Ok(Self {
|
||||
surface_state,
|
||||
acknowledged_first_configure: false,
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
surface,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
@@ -422,10 +391,6 @@ 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();
|
||||
@@ -483,10 +448,10 @@ impl WaylandWindow {
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
appearance: WindowAppearance,
|
||||
parent: Option<WaylandWindowStatePtr>,
|
||||
parent: Option<XdgToplevel>,
|
||||
) -> anyhow::Result<(Self, ObjectId)> {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone())?;
|
||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent)?;
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -508,7 +473,6 @@ impl WaylandWindow {
|
||||
globals,
|
||||
gpu_context,
|
||||
params,
|
||||
parent,
|
||||
)?)),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
});
|
||||
@@ -537,16 +501,6 @@ 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());
|
||||
@@ -864,9 +818,6 @@ 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);
|
||||
@@ -943,21 +894,6 @@ 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()
|
||||
@@ -965,9 +901,6 @@ 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
|
||||
{
|
||||
@@ -1092,26 +1025,13 @@ impl PlatformWindow for WaylandWindow {
|
||||
fn resize(&mut self, size: Size<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let state_ptr = self.0.clone();
|
||||
|
||||
// Keep window geometry consistent with configure handling. On Wayland, window geometry is
|
||||
// surface-local: resizing should not attempt to translate the window; the compositor
|
||||
// controls placement. We also account for client-side decoration insets and tiling.
|
||||
let window_geometry = inset_by_tiling(
|
||||
Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
},
|
||||
state.inset(),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
let dp_size = size.to_device_pixels(self.scale_factor());
|
||||
|
||||
state.surface_state.set_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
state.bounds.origin.x.0 as i32,
|
||||
state.bounds.origin.y.0 as i32,
|
||||
dp_size.width.0,
|
||||
dp_size.height.0,
|
||||
);
|
||||
|
||||
state
|
||||
|
||||
@@ -222,7 +222,7 @@ pub struct X11ClientState {
|
||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11ClientStatePtr {
|
||||
pub fn get_client(&self) -> Option<X11Client> {
|
||||
fn get_client(&self) -> Option<X11Client> {
|
||||
self.0.upgrade().map(X11Client)
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||
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 && window.should_close() {
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
// window "x" button clicked by user
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
drop(state);
|
||||
window.close();
|
||||
state = self.0.borrow_mut();
|
||||
if window.should_close() {
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
||||
window.state.borrow_mut().last_sync_counter =
|
||||
Some(x11rb::protocol::sync::Int64 {
|
||||
@@ -1216,33 +1216,6 @@ 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),
|
||||
@@ -1516,7 +1489,7 @@ impl LinuxClient for X11Client {
|
||||
let parent_window = state
|
||||
.keyboard_focused_window
|
||||
.and_then(|focused_window| state.windows.get(&focused_window))
|
||||
.map(|w| w.window.clone());
|
||||
.map(|window| window.window.x_window);
|
||||
let x_window = state
|
||||
.xcb_connection
|
||||
.generate_id()
|
||||
@@ -1571,15 +1544,7 @@ impl LinuxClient for X11Client {
|
||||
.cursor_styles
|
||||
.get(&focused_window)
|
||||
.unwrap_or(&CursorStyle::Arrow);
|
||||
|
||||
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 {
|
||||
if *current_style == style {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use crate::{
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::FxHashSet;
|
||||
use raw_window_handle as rwh;
|
||||
use util::{ResultExt, maybe};
|
||||
use x11rb::{
|
||||
@@ -75,7 +74,6 @@ 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,
|
||||
@@ -251,8 +249,6 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
parent: Option<X11WindowStatePtr>,
|
||||
children: FxHashSet<xproto::Window>,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -398,7 +394,7 @@ impl X11WindowState {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
parent_window: Option<xproto::Window>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let x_screen_index = params
|
||||
.display_id
|
||||
@@ -550,8 +546,8 @@ impl X11WindowState {
|
||||
)?;
|
||||
}
|
||||
|
||||
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
|
||||
if params.kind == WindowKind::Floating {
|
||||
if let Some(parent_window) = parent_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.
|
||||
@@ -567,23 +563,11 @@ 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 dialog window failed.",
|
||||
|| "X11 ChangeProperty32 setting window type for floating window failed.",
|
||||
xcb.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
@@ -592,20 +576,6 @@ 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(
|
||||
@@ -697,8 +667,6 @@ impl X11WindowState {
|
||||
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
||||
|
||||
Ok(Self {
|
||||
parent,
|
||||
children: FxHashSet::default(),
|
||||
client,
|
||||
executor,
|
||||
display,
|
||||
@@ -752,11 +720,6 @@ 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!({
|
||||
@@ -771,6 +734,8 @@ 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();
|
||||
@@ -808,7 +773,7 @@ impl X11Window {
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
parent_window: Option<X11WindowStatePtr>,
|
||||
parent_window: Option<xproto::Window>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let ptr = X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
@@ -1014,31 +979,7 @@ 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()
|
||||
@@ -1053,9 +994,6 @@ 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
|
||||
{
|
||||
@@ -1078,9 +1016,6 @@ 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);
|
||||
@@ -1091,9 +1026,6 @@ 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);
|
||||
@@ -1104,9 +1036,6 @@ 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);
|
||||
@@ -1117,9 +1046,6 @@ 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);
|
||||
|
||||
@@ -62,12 +62,9 @@ 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;
|
||||
@@ -426,8 +423,6 @@ 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 {
|
||||
@@ -627,16 +622,11 @@ impl MacWindow {
|
||||
}
|
||||
|
||||
let native_window: id = match kind {
|
||||
WindowKind::Normal => {
|
||||
msg_send![WINDOW_CLASS, alloc]
|
||||
}
|
||||
WindowKind::Normal | WindowKind::Floating => 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
|
||||
@@ -739,7 +729,6 @@ impl MacWindow {
|
||||
select_previous_tab_callback: None,
|
||||
toggle_tab_bar_callback: None,
|
||||
activated_least_once: false,
|
||||
sheet_parent: None,
|
||||
})));
|
||||
|
||||
(*native_window).set_ivar(
|
||||
@@ -790,18 +779,9 @@ 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 => {
|
||||
if kind == WindowKind::Floating {
|
||||
// Let the window float keep above normal windows.
|
||||
native_window.setLevel_(NSFloatingWindowLevel);
|
||||
} else {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
}
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
|
||||
if let Some(tabbing_identifier) = tabbing_identifier {
|
||||
@@ -836,23 +816,10 @@ 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
|
||||
@@ -894,11 +861,7 @@ 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);
|
||||
{
|
||||
let mut window_state = window.0.lock();
|
||||
window_state.move_traffic_light();
|
||||
window_state.sheet_parent = sheet_parent;
|
||||
}
|
||||
window.0.lock().move_traffic_light();
|
||||
|
||||
pool.drain();
|
||||
|
||||
@@ -975,7 +938,6 @@ 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);
|
||||
@@ -984,9 +946,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
TestDisplay, TestPlatformWindow, WindowAppearance, WindowParams, size,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
@@ -26,7 +26,7 @@ pub(crate) struct TestPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
|
||||
pub(crate) active_window: RefCell<Option<TestPlatformWindow>>,
|
||||
pub(crate) active_window: RefCell<Option<TestWindow>>,
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
@@ -196,7 +196,7 @@ impl TestPlatform {
|
||||
rx
|
||||
}
|
||||
|
||||
pub(crate) fn set_active_window(&self, window: Option<TestPlatformWindow>) {
|
||||
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
|
||||
let executor = self.foreground_executor();
|
||||
let previous_window = self.active_window.borrow_mut().take();
|
||||
self.active_window.borrow_mut().clone_from(&window);
|
||||
@@ -314,7 +314,7 @@ impl Platform for TestPlatform {
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
||||
let window = TestPlatformWindow::new(
|
||||
let window = TestWindow::new(
|
||||
handle,
|
||||
params,
|
||||
self.weak.clone(),
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
sync::{self, Arc},
|
||||
};
|
||||
|
||||
pub(crate) struct TestPlatformWindowState {
|
||||
pub(crate) struct TestWindowState {
|
||||
pub(crate) bounds: Bounds<Pixels>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
@@ -32,9 +32,9 @@ pub(crate) struct TestPlatformWindowState {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TestPlatformWindow(pub(crate) Rc<Mutex<TestPlatformWindowState>>);
|
||||
pub(crate) struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
|
||||
|
||||
impl HasWindowHandle for TestPlatformWindow {
|
||||
impl HasWindowHandle for TestWindow {
|
||||
fn window_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||
@@ -42,7 +42,7 @@ impl HasWindowHandle for TestPlatformWindow {
|
||||
}
|
||||
}
|
||||
|
||||
impl HasDisplayHandle for TestPlatformWindow {
|
||||
impl HasDisplayHandle for TestWindow {
|
||||
fn display_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||
@@ -50,14 +50,14 @@ impl HasDisplayHandle for TestPlatformWindow {
|
||||
}
|
||||
}
|
||||
|
||||
impl TestPlatformWindow {
|
||||
impl TestWindow {
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
platform: Weak<TestPlatform>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
) -> Self {
|
||||
Self(Rc::new(Mutex::new(TestPlatformWindowState {
|
||||
Self(Rc::new(Mutex::new(TestWindowState {
|
||||
bounds: params.bounds,
|
||||
display,
|
||||
platform,
|
||||
@@ -111,7 +111,7 @@ impl TestPlatformWindow {
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for TestPlatformWindow {
|
||||
impl PlatformWindow for TestWindow {
|
||||
fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.lock().bounds
|
||||
}
|
||||
@@ -272,7 +272,7 @@ impl PlatformWindow for TestPlatformWindow {
|
||||
self.0.lock().sprite_atlas.clone()
|
||||
}
|
||||
|
||||
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,6 @@ 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),
|
||||
@@ -270,14 +265,6 @@ 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() == ERROR_NOT_FOUND.to_hresult() {
|
||||
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(err.into());
|
||||
|
||||
@@ -83,7 +83,6 @@ 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 {
|
||||
@@ -242,7 +241,6 @@ 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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -370,7 +368,6 @@ struct WindowCreateContext {
|
||||
disable_direct_composition: bool,
|
||||
directx_devices: DirectXDevices,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
parent_hwnd: Option<HWND>,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -393,20 +390,6 @@ 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()
|
||||
@@ -433,14 +416,8 @@ 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
|
||||
};
|
||||
|
||||
(dwexstyle, dwstyle)
|
||||
(WS_EX_APPWINDOW, dwstyle)
|
||||
};
|
||||
if !disable_direct_composition {
|
||||
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
||||
@@ -472,7 +449,6 @@ impl WindowsWindow {
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
parent_hwnd,
|
||||
};
|
||||
let creation_result = unsafe {
|
||||
CreateWindowExW(
|
||||
@@ -484,7 +460,7 @@ impl WindowsWindow {
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
parent_hwnd,
|
||||
None,
|
||||
None,
|
||||
Some(hinstance.into()),
|
||||
Some(&context as *const _ as *const _),
|
||||
|
||||
@@ -20,126 +20,6 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
||||
|
||||
pub(crate) type DrawOrder = u32;
|
||||
|
||||
/// Test-only scene snapshot for inspecting rendered content.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test_scene {
|
||||
use crate::{Bounds, Hsla, Point, ScaledPixels, SharedString};
|
||||
|
||||
/// A rendered quad (background, border, cursor, selection, etc.)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedQuad {
|
||||
/// Bounds in scaled pixels.
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
/// Background color (if solid).
|
||||
pub background_color: Option<Hsla>,
|
||||
/// Border color.
|
||||
pub border_color: Hsla,
|
||||
}
|
||||
|
||||
/// A named diagnostic quad for tests and debugging of imperative paint logic.
|
||||
///
|
||||
/// This is not necessarily a "real" painted quad; it is metadata recorded alongside a scene.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiagnosticQuad {
|
||||
/// A stable name that test code can filter by.
|
||||
pub name: SharedString,
|
||||
/// Bounds in scaled pixels.
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
/// Optional color hint (useful when visualizing).
|
||||
pub color: Option<Hsla>,
|
||||
}
|
||||
|
||||
/// A rendered text glyph.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedGlyph {
|
||||
/// Origin position in scaled pixels.
|
||||
pub origin: Point<ScaledPixels>,
|
||||
/// Size in scaled pixels.
|
||||
pub size: crate::Size<ScaledPixels>,
|
||||
/// Color of the glyph.
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
||||
/// Snapshot of scene contents for testing.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SceneSnapshot {
|
||||
/// All rendered quads.
|
||||
pub quads: Vec<RenderedQuad>,
|
||||
/// All rendered text glyphs.
|
||||
pub glyphs: Vec<RenderedGlyph>,
|
||||
/// Named diagnostic quads recorded by imperative drawing code for tests/debugging.
|
||||
pub diagnostic_quads: Vec<DiagnosticQuad>,
|
||||
/// Number of shadow primitives.
|
||||
pub shadow_count: usize,
|
||||
/// Number of path primitives.
|
||||
pub path_count: usize,
|
||||
/// Number of underline primitives.
|
||||
pub underline_count: usize,
|
||||
/// Number of polychrome sprites (images, emoji).
|
||||
pub polychrome_sprite_count: usize,
|
||||
/// Number of surface primitives.
|
||||
pub surface_count: usize,
|
||||
}
|
||||
|
||||
impl SceneSnapshot {
|
||||
/// Get unique Y positions of quads, sorted.
|
||||
pub fn quad_y_positions(&self) -> Vec<f32> {
|
||||
let mut positions: Vec<f32> = self.quads.iter().map(|q| q.bounds.origin.y.0).collect();
|
||||
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
positions.dedup();
|
||||
positions
|
||||
}
|
||||
|
||||
/// Get unique Y positions of glyphs, sorted.
|
||||
pub fn glyph_y_positions(&self) -> Vec<f32> {
|
||||
let mut positions: Vec<f32> = self.glyphs.iter().map(|g| g.origin.y.0).collect();
|
||||
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
positions.dedup();
|
||||
positions
|
||||
}
|
||||
|
||||
/// Find quads within a Y range.
|
||||
pub fn quads_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedQuad> {
|
||||
self.quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
let y = q.bounds.origin.y.0;
|
||||
y >= min_y && y < max_y
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find glyphs within a Y range.
|
||||
pub fn glyphs_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedGlyph> {
|
||||
self.glyphs
|
||||
.iter()
|
||||
.filter(|g| {
|
||||
let y = g.origin.y.0;
|
||||
y >= min_y && y < max_y
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Debug summary string.
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"quads: {}, glyphs: {}, diagnostic_quads: {}, shadows: {}, paths: {}, underlines: {}, polychrome: {}, surfaces: {}",
|
||||
self.quads.len(),
|
||||
self.glyphs.len(),
|
||||
self.diagnostic_quads.len(),
|
||||
self.shadow_count,
|
||||
self.path_count,
|
||||
self.underline_count,
|
||||
self.polychrome_sprite_count,
|
||||
self.surface_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test_scene::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Scene {
|
||||
pub(crate) paint_operations: Vec<PaintOperation>,
|
||||
@@ -152,8 +32,6 @@ pub(crate) struct Scene {
|
||||
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
||||
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
||||
pub(crate) surfaces: Vec<PaintSurface>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) diagnostic_quads: Vec<test_scene::DiagnosticQuad>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@@ -168,8 +46,6 @@ impl Scene {
|
||||
self.monochrome_sprites.clear();
|
||||
self.polychrome_sprites.clear();
|
||||
self.surfaces.clear();
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
self.diagnostic_quads.clear();
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -248,41 +124,6 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a snapshot of the scene for testing.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn snapshot(&self) -> SceneSnapshot {
|
||||
let quads = self
|
||||
.quads
|
||||
.iter()
|
||||
.map(|q| RenderedQuad {
|
||||
bounds: q.bounds,
|
||||
background_color: q.background.as_solid(),
|
||||
border_color: q.border_color,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let glyphs = self
|
||||
.monochrome_sprites
|
||||
.iter()
|
||||
.map(|s| RenderedGlyph {
|
||||
origin: s.bounds.origin,
|
||||
size: s.bounds.size,
|
||||
color: s.color,
|
||||
})
|
||||
.collect();
|
||||
|
||||
SceneSnapshot {
|
||||
quads,
|
||||
glyphs,
|
||||
diagnostic_quads: self.diagnostic_quads.clone(),
|
||||
shadow_count: self.shadows.len(),
|
||||
path_count: self.paths.len(),
|
||||
underline_count: self.underlines.len(),
|
||||
polychrome_sprite_count: self.polychrome_sprites.len(),
|
||||
surface_count: self.surfaces.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) {
|
||||
self.shadows.sort_by_key(|shadow| shadow.order);
|
||||
self.quads.sort_by_key(|quad| quad.order);
|
||||
@@ -293,10 +134,6 @@ impl Scene {
|
||||
self.polychrome_sprites
|
||||
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
||||
self.surfaces.sort_by_key(|surface| surface.order);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
self.diagnostic_quads
|
||||
.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
@@ -783,7 +620,7 @@ impl Default for TransformationMatrix {
|
||||
#[repr(C)]
|
||||
pub(crate) struct MonochromeSprite {
|
||||
pub order: DrawOrder,
|
||||
pub pad: u32,
|
||||
pub pad: u32, // align to 8 bytes
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
@@ -801,7 +638,7 @@ impl From<MonochromeSprite> for Primitive {
|
||||
#[repr(C)]
|
||||
pub(crate) struct PolychromeSprite {
|
||||
pub order: DrawOrder,
|
||||
pub pad: u32,
|
||||
pub pad: u32, // align to 8 bytes
|
||||
pub grayscale: bool,
|
||||
pub opacity: f32,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
|
||||
@@ -506,10 +506,6 @@ impl HitboxId {
|
||||
///
|
||||
/// See [`Hitbox::is_hovered`] for details.
|
||||
pub fn is_hovered(self, window: &Window) -> bool {
|
||||
// If this hitbox has captured the pointer, it's always considered hovered
|
||||
if window.captured_hitbox == Some(self) {
|
||||
return true;
|
||||
}
|
||||
let hit_test = &window.mouse_hit_test;
|
||||
for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
|
||||
if self == *id {
|
||||
@@ -764,11 +760,6 @@ impl Frame {
|
||||
self.tab_stops.clear();
|
||||
self.focus = None;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
self.debug_bounds.clear();
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
{
|
||||
self.next_inspector_instance_ids.clear();
|
||||
@@ -896,9 +887,6 @@ pub struct Window {
|
||||
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
|
||||
prompt: Option<RenderablePromptHandle>,
|
||||
pub(crate) client_inset: Option<Pixels>,
|
||||
/// The hitbox that has captured the pointer, if any.
|
||||
/// While captured, mouse events route to this hitbox regardless of hit testing.
|
||||
captured_hitbox: Option<HitboxId>,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
inspector: Option<Entity<Inspector>>,
|
||||
}
|
||||
@@ -1323,7 +1311,6 @@ impl Window {
|
||||
prompt: None,
|
||||
client_inset: None,
|
||||
image_cache_stack: Vec::new(),
|
||||
captured_hitbox: None,
|
||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||
inspector: None,
|
||||
})
|
||||
@@ -2007,26 +1994,6 @@ impl Window {
|
||||
self.mouse_position
|
||||
}
|
||||
|
||||
/// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up
|
||||
/// events will be routed to listeners that check this hitbox's `is_hovered` status,
|
||||
/// regardless of actual hit testing. This enables drag operations that continue
|
||||
/// even when the pointer moves outside the element's bounds.
|
||||
///
|
||||
/// The capture is automatically released on mouse up.
|
||||
pub fn capture_pointer(&mut self, hitbox_id: HitboxId) {
|
||||
self.captured_hitbox = Some(hitbox_id);
|
||||
}
|
||||
|
||||
/// Releases any active pointer capture.
|
||||
pub fn release_pointer(&mut self) {
|
||||
self.captured_hitbox = None;
|
||||
}
|
||||
|
||||
/// Returns the hitbox that has captured the pointer, if any.
|
||||
pub fn captured_hitbox(&self) -> Option<HitboxId> {
|
||||
self.captured_hitbox
|
||||
}
|
||||
|
||||
/// The current state of the keyboard's modifiers
|
||||
pub fn modifiers(&self) -> Modifiers {
|
||||
self.modifiers
|
||||
@@ -2999,41 +2966,6 @@ impl Window {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
/// Record a named diagnostic quad for test/debug snapshots.
|
||||
///
|
||||
/// This is intended for debugging and asserting against imperative painting logic. The
|
||||
/// recorded quad does not affect rendering; it is captured alongside the rendered scene and
|
||||
/// exposed via `scene_snapshot()`.
|
||||
pub fn record_diagnostic_quad(
|
||||
&mut self,
|
||||
name: impl Into<SharedString>,
|
||||
bounds: Bounds<Pixels>,
|
||||
color: Option<Hsla>,
|
||||
) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
|
||||
let scale_factor = self.scale_factor();
|
||||
self.next_frame.scene.diagnostic_quads.push(crate::test_scene::DiagnosticQuad {
|
||||
name: name.into(),
|
||||
bounds: bounds.scale(scale_factor),
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
#[inline]
|
||||
/// Record a named diagnostic quad for test/debug snapshots.
|
||||
///
|
||||
/// This is a no-op unless tests or the `test-support` feature are enabled.
|
||||
pub fn record_diagnostic_quad(
|
||||
&mut self,
|
||||
_name: impl Into<SharedString>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_color: Option<Hsla>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||
///
|
||||
/// This method should only be called as part of the paint phase of element drawing.
|
||||
@@ -3918,11 +3850,6 @@ impl Window {
|
||||
self.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-release pointer capture on mouse up
|
||||
if event.is::<MouseUpEvent>() && self.captured_hitbox.is_some() {
|
||||
self.captured_hitbox = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {
|
||||
@@ -5039,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, Debug)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct AnyWindowHandle {
|
||||
pub(crate) id: WindowId,
|
||||
state_type: TypeId,
|
||||
|
||||
@@ -1801,7 +1801,9 @@ impl Buffer {
|
||||
self.syntax_map.lock().did_parse(syntax_snapshot);
|
||||
self.request_autoindent(cx);
|
||||
self.parse_status.0.send(ParseStatus::Idle).unwrap();
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
if self.text.version() != *self.tree_sitter_data.version() {
|
||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||
}
|
||||
cx.emit(BufferEvent::Reparsed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -295,23 +295,6 @@ 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.len() > 1
|
||||
&& !snippet.tabstops.is_empty()
|
||||
{
|
||||
label = String::new();
|
||||
|
||||
@@ -421,9 +421,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
0..label.rfind('(').unwrap_or(completion.label.len()),
|
||||
highlight_id,
|
||||
));
|
||||
} else if detail_left.is_none()
|
||||
&& kind != Some(lsp::CompletionItemKind::SNIPPET)
|
||||
{
|
||||
} else if detail_left.is_none() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -1599,40 +1597,6 @@ 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 => 256000,
|
||||
Self::MistralLargeLatest => 131000,
|
||||
Self::MistralMediumLatest => 128000,
|
||||
Self::MistralSmallLatest => 32000,
|
||||
Self::MagistralMediumLatest => 128000,
|
||||
Self::MagistralSmallLatest => 128000,
|
||||
Self::MagistralMediumLatest => 40000,
|
||||
Self::MagistralSmallLatest => 40000,
|
||||
Self::OpenMistralNemo => 131000,
|
||||
Self::OpenCodestralMamba => 256000,
|
||||
Self::DevstralMediumLatest => 256000,
|
||||
Self::DevstralSmallLatest => 256000,
|
||||
Self::DevstralMediumLatest => 128000,
|
||||
Self::DevstralSmallLatest => 262144,
|
||||
Self::Pixtral12BLatest => 128000,
|
||||
Self::PixtralLargeLatest => 128000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
|
||||
@@ -2610,8 +2610,9 @@ 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)) + 1usize)
|
||||
.min(snapshot.len());
|
||||
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);
|
||||
cursor.seek(&start, Bias::Right);
|
||||
while let Some(item) = cursor.item() {
|
||||
if *cursor.start() >= end {
|
||||
|
||||
@@ -460,7 +460,7 @@ impl AgentServerStore {
|
||||
.gemini
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(true),
|
||||
.unwrap_or(false),
|
||||
}),
|
||||
);
|
||||
self.external_agents.insert(
|
||||
|
||||
@@ -1672,6 +1672,59 @@ 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,
|
||||
@@ -4200,6 +4253,28 @@ 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>,
|
||||
@@ -4208,6 +4283,9 @@ 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
|
||||
@@ -4273,6 +4351,9 @@ 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
|
||||
|
||||
@@ -790,7 +790,8 @@ impl TerminalPanel {
|
||||
}
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||
let focus = pane.has_focus(window, cx)
|
||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||
});
|
||||
|
||||
@@ -852,7 +853,8 @@ impl TerminalPanel {
|
||||
}
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||
let focus = pane.has_focus(window, cx)
|
||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||
});
|
||||
|
||||
@@ -1169,67 +1171,64 @@ pub fn new_terminal_pane(
|
||||
let source = tab.pane.clone();
|
||||
let item_id_to_move = item.item_id();
|
||||
|
||||
// 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(());
|
||||
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(());
|
||||
};
|
||||
|
||||
// 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,
|
||||
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,
|
||||
split_direction,
|
||||
item_id_to_move,
|
||||
new_pane.read(cx).active_item_index(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
);
|
||||
})
|
||||
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();
|
||||
.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 if let Some(project_path) = item.project_path(cx)
|
||||
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@ mod terminal_slash_command;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use persistence::TERMINAL_DB;
|
||||
@@ -687,32 +687,12 @@ impl TerminalView {
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if clipboard.entries().iter().any(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => !image.bytes.is_empty(),
|
||||
_ => false,
|
||||
}) {
|
||||
self.forward_ctrl_v(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(text) = clipboard.text() {
|
||||
if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&text));
|
||||
.update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
|
||||
/// and attach images using their native workflows.
|
||||
fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
|
||||
self.terminal.update(cx, |term, _| {
|
||||
term.input(vec![0x16]);
|
||||
});
|
||||
}
|
||||
|
||||
fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.clear_bell(cx);
|
||||
self.terminal.update(cx, |term, _| {
|
||||
|
||||
@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
|
||||
Severity::Info => (
|
||||
IconName::Info,
|
||||
Color::Muted,
|
||||
cx.theme().status().info_background.opacity(0.1),
|
||||
cx.theme().colors().panel_background.opacity(0.),
|
||||
),
|
||||
Severity::Success => (
|
||||
IconName::Check,
|
||||
|
||||
@@ -1303,6 +1303,39 @@ async fn test_mouse_selection(cx: &mut TestAppContext) {
|
||||
cx.assert_state("one «ˇtwo» three", Mode::Visual)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_drag_across_anchor_does_not_drift(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("ˇone two three four", Mode::Normal);
|
||||
|
||||
let click_pos = cx.pixel_position("one ˇtwo three four");
|
||||
let drag_left = cx.pixel_position("ˇone two three four");
|
||||
let anchor_pos = cx.pixel_position("one tˇwo three four");
|
||||
|
||||
cx.simulate_mouse_down(click_pos, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
|
||||
|
||||
cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
|
||||
|
||||
cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
|
||||
cx.run_until_parked();
|
||||
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
|
||||
|
||||
cx.simulate_mouse_up(drag_left, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
|
||||
#[perf]
|
||||
#[gpui::test]
|
||||
async fn test_lowercase_marks(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -23,8 +23,9 @@ use crate::normal::paste::Paste as VimPaste;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
|
||||
SelectionEffects, ToPoint,
|
||||
SelectionEffects,
|
||||
actions::Paste,
|
||||
display_map::ToDisplayPoint,
|
||||
movement::{self, FindRange},
|
||||
};
|
||||
use gpui::{
|
||||
@@ -37,6 +38,8 @@ use language::{
|
||||
};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use multi_buffer::ToPoint as _;
|
||||
use multi_buffer::ToPoint as _;
|
||||
use normal::search::SearchSubmit;
|
||||
use object::Object;
|
||||
use schemars::JsonSchema;
|
||||
@@ -503,6 +506,7 @@ pub(crate) struct Vim {
|
||||
pub(crate) current_anchor: Option<Selection<Anchor>>,
|
||||
pub(crate) undo_modes: HashMap<TransactionId, Mode>,
|
||||
pub(crate) undo_last_line_tx: Option<TransactionId>,
|
||||
extended_pending_selection_id: Option<usize>,
|
||||
|
||||
selected_register: Option<char>,
|
||||
pub search: SearchState,
|
||||
@@ -561,6 +565,7 @@ impl Vim {
|
||||
current_tx: None,
|
||||
undo_last_line_tx: None,
|
||||
current_anchor: None,
|
||||
extended_pending_selection_id: None,
|
||||
undo_modes: HashMap::default(),
|
||||
|
||||
status_label: None,
|
||||
@@ -1218,17 +1223,32 @@ impl Vim {
|
||||
s.select_anchor_ranges(vec![pos..pos])
|
||||
}
|
||||
|
||||
let snapshot = s.display_snapshot();
|
||||
if let Some(pending) = s.pending_anchor_mut()
|
||||
&& pending.reversed
|
||||
let mut should_extend_pending = false;
|
||||
if !last_mode.is_visual()
|
||||
&& mode.is_visual()
|
||||
&& !last_mode.is_visual()
|
||||
&& let Some(pending) = s.pending_anchor()
|
||||
{
|
||||
let mut end = pending.end.to_point(&snapshot.buffer_snapshot());
|
||||
end = snapshot
|
||||
.buffer_snapshot()
|
||||
.clip_point(end + Point::new(0, 1), Bias::Right);
|
||||
pending.end = snapshot.buffer_snapshot().anchor_before(end);
|
||||
let snapshot = s.display_snapshot();
|
||||
let is_empty = pending
|
||||
.start
|
||||
.cmp(&pending.end, &snapshot.buffer_snapshot())
|
||||
.is_eq();
|
||||
should_extend_pending = pending.reversed
|
||||
&& !is_empty
|
||||
&& vim.extended_pending_selection_id != Some(pending.id);
|
||||
};
|
||||
|
||||
if should_extend_pending {
|
||||
let snapshot = s.display_snapshot();
|
||||
if let Some(pending) = s.pending_anchor_mut() {
|
||||
let end = pending.end.to_point(&snapshot.buffer_snapshot());
|
||||
let end = end.to_display_point(&snapshot);
|
||||
let new_end = movement::right(&snapshot, end);
|
||||
pending.end = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_before(new_end.to_point(&snapshot));
|
||||
}
|
||||
vim.extended_pending_selection_id = s.pending_anchor().map(|p| p.id)
|
||||
}
|
||||
|
||||
s.move_with(|map, selection| {
|
||||
@@ -1240,8 +1260,10 @@ impl Vim {
|
||||
point = map.clip_point(point, Bias::Left);
|
||||
}
|
||||
selection.collapse_to(point, selection.goal)
|
||||
} else if !last_mode.is_visual() && mode.is_visual() && selection.is_empty() {
|
||||
selection.end = movement::right(map, selection.start);
|
||||
} else if !last_mode.is_visual() && mode.is_visual() {
|
||||
if selection.is_empty() {
|
||||
selection.end = movement::right(map, selection.start);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -886,12 +886,8 @@ impl<T: Item> ItemHandle for Entity<T> {
|
||||
// Only trigger autosave if focus has truly left the item.
|
||||
// If focus is still within the item's hierarchy (e.g., moved to a context menu),
|
||||
// don't trigger autosave to avoid unwanted formatting and cursor jumps.
|
||||
// Also skip autosave if focus moved to a modal (e.g., command palette),
|
||||
// since the user is still interacting with the workspace.
|
||||
let focus_handle = item.item_focus_handle(cx);
|
||||
if !focus_handle.contains_focused(window, cx)
|
||||
&& !workspace.has_active_modal(window, cx)
|
||||
{
|
||||
if !focus_handle.contains_focused(window, cx) {
|
||||
Pane::autosave_item(&item, workspace.project.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -193,12 +193,6 @@ 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,53 +3296,4 @@ 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,18 +1748,26 @@ 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()
|
||||
&& let Some(display) = workspace.display
|
||||
&& let Some(bounds) = workspace.window_bounds.as_ref()
|
||||
{
|
||||
} else if let Some(workspace) = serialized_workspace.as_ref() {
|
||||
// Reopening an existing workspace - restore its saved bounds
|
||||
(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))
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
// New window - let GPUI's default_bounds() handle cascading
|
||||
(None, None)
|
||||
@@ -5665,24 +5673,12 @@ impl Workspace {
|
||||
persistence::DB.save_workspace(serialized_workspace).await;
|
||||
})
|
||||
}
|
||||
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::DetachFromSession => window.spawn(cx, async move |_| {
|
||||
persistence::DB
|
||||
.set_session_id(database_id, None)
|
||||
.await
|
||||
.log_err();
|
||||
}),
|
||||
WorkspaceLocation::None => Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -354,8 +354,6 @@ pub mod agent {
|
||||
ResetAgentZoom,
|
||||
/// Toggles the utility/agent pane open/closed state.
|
||||
ToggleAgentPane,
|
||||
/// Pastes clipboard content without any formatting.
|
||||
PasteRaw,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,6 @@ 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)
|
||||
|
||||
@@ -106,17 +106,6 @@ mv Cargo.toml.backup Cargo.toml
|
||||
popd
|
||||
echo "Bundled ${app_path}"
|
||||
|
||||
# DocumentTypes.plist references CFBundleTypeIconFile "Document", so the bundle must contain Document.icns.
|
||||
# We use the app icon as a placeholder document icon for now.
|
||||
document_icon_source="crates/zed/resources/Document.icns"
|
||||
document_icon_target="${app_path}/Contents/Resources/Document.icns"
|
||||
if [[ -f "${document_icon_source}" ]]; then
|
||||
mkdir -p "$(dirname "${document_icon_target}")"
|
||||
cp "${document_icon_source}" "${document_icon_target}"
|
||||
else
|
||||
echo "cargo::warning=Missing ${document_icon_source}; macOS document icons may not appear in Finder."
|
||||
fi
|
||||
|
||||
if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
|
||||
can_code_sign=true
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
script/verify-macos-document-icon /path/to/Zed.app
|
||||
|
||||
Verifies that the given macOS app bundle's Info.plist references a document icon
|
||||
named "Document" and that the corresponding icon file exists in the bundle.
|
||||
|
||||
Specifically checks:
|
||||
- CFBundleDocumentTypes[*].CFBundleTypeIconFile includes "Document"
|
||||
- Contents/Resources/Document.icns exists
|
||||
|
||||
Exit codes:
|
||||
0 - success
|
||||
1 - verification failed
|
||||
2 - invalid usage / missing prerequisites
|
||||
USAGE
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
app_path="$1"
|
||||
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
fail "app bundle not found: ${app_path}"
|
||||
fi
|
||||
|
||||
info_plist="${app_path}/Contents/Info.plist"
|
||||
if [[ ! -f "${info_plist}" ]]; then
|
||||
fail "missing Info.plist: ${info_plist}"
|
||||
fi
|
||||
|
||||
if ! command -v plutil >/dev/null 2>&1; then
|
||||
fail "plutil not found (required on macOS to read Info.plist)"
|
||||
fi
|
||||
|
||||
# Convert to JSON for robust parsing. plutil outputs JSON to stdout in this mode.
|
||||
info_json="$(plutil -convert json -o - "${info_plist}")"
|
||||
|
||||
# Check that CFBundleDocumentTypes exists and that at least one entry references "Document".
|
||||
# We use Python for JSON parsing; macOS ships with Python 3 on many setups, but not all.
|
||||
# If python3 isn't available, fall back to a simpler grep-based check.
|
||||
has_document_icon_ref="false"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
has_document_icon_ref="$(python3 -c "import json,sys; d=json.load(sys.stdin); types=d.get('CFBundleDocumentTypes', []); vals=[t.get('CFBundleTypeIconFile') for t in types if isinstance(t, dict)]; print('true' if 'Document' in vals else 'false')" <<<"${info_json}")"
|
||||
else
|
||||
# This is a best-effort fallback. It may produce false negatives if the JSON formatting differs.
|
||||
if echo "${info_json}" | grep -q '"CFBundleTypeIconFile"[[:space:]]*:[[:space:]]*"Document"'; then
|
||||
has_document_icon_ref="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${has_document_icon_ref}" != "true" ]]; then
|
||||
echo "Verification failed for: ${app_path}" >&2
|
||||
echo "Expected Info.plist to reference CFBundleTypeIconFile \"Document\" in CFBundleDocumentTypes." >&2
|
||||
echo "Tip: This bundle may be missing DocumentTypes.plist extensions or may have different icon naming." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
document_icon_path="${app_path}/Contents/Resources/Document.icns"
|
||||
if [[ ! -f "${document_icon_path}" ]]; then
|
||||
echo "Verification failed for: ${app_path}" >&2
|
||||
echo "Expected document icon to exist: ${document_icon_path}" >&2
|
||||
echo "Tip: The bundle script should copy crates/zed/resources/Document.icns into Contents/Resources/Document.icns." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: ${app_path}"
|
||||
echo " - Info.plist references CFBundleTypeIconFile \"Document\""
|
||||
echo " - Found ${document_icon_path}"
|
||||
@@ -45,15 +45,11 @@ 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_tests_linux),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Linux)),
|
||||
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
||||
should_run_tests.guard(doctests()),
|
||||
should_run_tests.guard(check_workspace_binaries()),
|
||||
@@ -110,7 +106,6 @@ 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
|
||||
@@ -226,8 +221,6 @@ 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(
|
||||
@@ -243,58 +236,15 @@ fn check_style() -> NamedJob {
|
||||
.add_step(steps::checkout_repo())
|
||||
.add_step(steps::cache_rust_dependencies_namespace())
|
||||
.add_step(steps::setup_pnpm())
|
||||
.add_step(steps::prettier())
|
||||
.add_step(steps::script("./script/prettier"))
|
||||
.add_step(steps::cargo_fmt())
|
||||
.add_step(steps::record_style_failure())
|
||||
.add_step(steps::trigger_autofix(false))
|
||||
.add_step(steps::script("./script/check-todos"))
|
||||
.add_step(steps::script("./script/check-keymaps"))
|
||||
.add_step(check_for_typos())
|
||||
.outputs([(
|
||||
STYLE_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_STYLE_FAILURE_STEP_ID
|
||||
),
|
||||
)]),
|
||||
.add_step(check_for_typos()),
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -355,8 +305,6 @@ 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,
|
||||
@@ -379,23 +327,12 @@ 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::record_clippy_failure())
|
||||
})
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.add_step(steps::cargo_install_nextest())
|
||||
job.add_step(steps::trigger_autofix(true))
|
||||
.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))
|
||||
.when(platform == Platform::Linux, |job| {
|
||||
job.outputs([(
|
||||
CLIPPY_FAILED_OUTPUT.to_owned(),
|
||||
format!(
|
||||
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||
steps::RECORD_CLIPPY_FAILURE_STEP_ID
|
||||
),
|
||||
)])
|
||||
}),
|
||||
.add_step(steps::cleanup_cargo_config(platform)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,25 +54,8 @@ 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").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()"))
|
||||
named::bash("cargo fmt --all -- --check")
|
||||
}
|
||||
|
||||
pub fn cargo_install_nextest() -> Step<Use> {
|
||||
@@ -118,25 +101,13 @@ 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").id(CLIPPY_STEP_ID),
|
||||
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
|
||||
Platform::Windows => named::pwsh("./script/clippy.ps1"),
|
||||
_ => named::bash("./script/clippy"),
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
@@ -374,6 +345,16 @@ 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