Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f34525634 | ||
|
|
2d071b0cb6 | ||
|
|
bd2b0de231 | ||
|
|
886de8f54b | ||
|
|
7a783a91cc | ||
|
|
f9462da2f7 | ||
|
|
61dd6a8f31 | ||
|
|
abb199c85e | ||
|
|
cebbf77491 | ||
|
|
0180f3e72a | ||
|
|
5488a19221 | ||
|
|
bb1198e7d6 | ||
|
|
69fe27f45e | ||
|
|
469da2fd07 | ||
|
|
4f87822133 | ||
|
|
9a69d89f88 | ||
|
|
54f360ace1 | ||
|
|
b2a0b78ece | ||
|
|
f1ca2f9f31 | ||
|
|
4b34adedd2 | ||
|
|
df48294caa | ||
|
|
cdc5cc348f | ||
|
|
0f7f540138 | ||
|
|
184001b33b | ||
|
|
225a2a8a20 | ||
|
|
ea37057814 | ||
|
|
77cdef3596 | ||
|
|
05108c50fd | ||
|
|
07538ff08e | ||
|
|
9073a2666c | ||
|
|
843a35a1a9 | ||
|
|
aff93f2f6c | ||
|
|
da6c2a172c | ||
|
|
f2409f2605 | ||
|
|
ce1c228e6e | ||
|
|
96ddbd4e13 | ||
|
|
f224d2a923 |
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,7 +61,8 @@ jobs:
|
|||||||
uses: namespacelabs/nscloud-cache-action@v1
|
uses: namespacelabs/nscloud-cache-action@v1
|
||||||
with:
|
with:
|
||||||
cache: rust
|
cache: rust
|
||||||
- name: steps::cargo_fmt
|
- id: cargo_fmt
|
||||||
|
name: steps::cargo_fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: extension_tests::run_clippy
|
- name: extension_tests::run_clippy
|
||||||
|
|||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy
|
run: ./script/clippy
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
@@ -71,15 +72,15 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy
|
run: ./script/clippy
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::trigger_autofix
|
- id: record_clippy_failure
|
||||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
name: steps::record_clippy_failure
|
||||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
if: always()
|
||||||
|
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: steps::cargo_install_nextest
|
- name: steps::cargo_install_nextest
|
||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
@@ -93,6 +94,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf ./../.cargo
|
rm -rf ./../.cargo
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
|
outputs:
|
||||||
|
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run_tests_windows:
|
run_tests_windows:
|
||||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||||
@@ -111,7 +114,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy.ps1
|
run: ./script/clippy.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
|
|||||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,7 +20,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
clean: false
|
clean: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: steps::cargo_fmt
|
- id: cargo_fmt
|
||||||
|
name: steps::cargo_fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: ./script/clippy
|
- name: ./script/clippy
|
||||||
@@ -44,7 +45,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy.ps1
|
run: ./script/clippy.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
|
|||||||
55
.github/workflows/run_tests.yml
vendored
55
.github/workflows/run_tests.yml
vendored
@@ -74,18 +74,19 @@ jobs:
|
|||||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||||
with:
|
with:
|
||||||
version: '9'
|
version: '9'
|
||||||
- name: ./script/prettier
|
- id: prettier
|
||||||
|
name: steps::prettier
|
||||||
run: ./script/prettier
|
run: ./script/prettier
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::cargo_fmt
|
- id: cargo_fmt
|
||||||
|
name: steps::cargo_fmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::trigger_autofix
|
- id: record_style_failure
|
||||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
name: steps::record_style_failure
|
||||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
|
if: always()
|
||||||
|
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: ./script/check-todos
|
- name: ./script/check-todos
|
||||||
run: ./script/check-todos
|
run: ./script/check-todos
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
@@ -96,6 +97,8 @@ jobs:
|
|||||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||||
with:
|
with:
|
||||||
config: ./typos.toml
|
config: ./typos.toml
|
||||||
|
outputs:
|
||||||
|
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run_tests_windows:
|
run_tests_windows:
|
||||||
needs:
|
needs:
|
||||||
@@ -116,7 +119,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy.ps1
|
run: ./script/clippy.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
@@ -163,15 +167,15 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy
|
run: ./script/clippy
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::trigger_autofix
|
- id: record_clippy_failure
|
||||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
name: steps::record_clippy_failure
|
||||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
if: always()
|
||||||
|
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: steps::cargo_install_nextest
|
- name: steps::cargo_install_nextest
|
||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
@@ -185,6 +189,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf ./../.cargo
|
rm -rf ./../.cargo
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
|
outputs:
|
||||||
|
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run_tests_mac:
|
run_tests_mac:
|
||||||
needs:
|
needs:
|
||||||
@@ -205,7 +211,8 @@ jobs:
|
|||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
- name: steps::clippy
|
- id: clippy
|
||||||
|
name: steps::clippy
|
||||||
run: ./script/clippy
|
run: ./script/clippy
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
- name: steps::clear_target_dir_if_large
|
- name: steps::clear_target_dir_if_large
|
||||||
@@ -585,6 +592,24 @@ jobs:
|
|||||||
|
|
||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
shell: bash -euxo pipefail {0}
|
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:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.9.0"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"agent-client-protocol-schema",
|
"agent-client-protocol-schema",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -243,9 +243,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol-schema"
|
name = "agent-client-protocol-schema"
|
||||||
version = "0.10.0"
|
version = "0.10.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
@@ -793,7 +793,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols 0.32.9",
|
"wayland-protocols",
|
||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7370,7 +7370,7 @@ dependencies = [
|
|||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-cursor",
|
"wayland-cursor",
|
||||||
"wayland-protocols 0.31.2",
|
"wayland-protocols",
|
||||||
"wayland-protocols-plasma",
|
"wayland-protocols-plasma",
|
||||||
"wayland-protocols-wlr",
|
"wayland-protocols-wlr",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
@@ -18927,18 +18927,6 @@ dependencies = [
|
|||||||
"xcursor",
|
"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]]
|
[[package]]
|
||||||
name = "wayland-protocols"
|
name = "wayland-protocols"
|
||||||
version = "0.32.9"
|
version = "0.32.9"
|
||||||
@@ -18953,14 +18941,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-plasma"
|
name = "wayland-protocols-plasma"
|
||||||
version = "0.2.0"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols 0.31.2",
|
"wayland-protocols",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -18973,7 +18961,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-protocols 0.32.9",
|
"wayland-protocols",
|
||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
|||||||
# External crates
|
# External crates
|
||||||
#
|
#
|
||||||
|
|
||||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = "0.25.1-rc1"
|
alacritty_terminal = "0.25.1-rc1"
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|||||||
@@ -227,6 +227,7 @@
|
|||||||
"ctrl-g": "search::SelectNextMatch",
|
"ctrl-g": "search::SelectNextMatch",
|
||||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -293,6 +294,7 @@
|
|||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
"ctrl-shift-y": "agent::KeepAll",
|
||||||
"ctrl-shift-n": "agent::RejectAll",
|
"ctrl-shift-n": "agent::RejectAll",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -304,6 +306,7 @@
|
|||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
"ctrl-shift-y": "agent::KeepAll",
|
||||||
"ctrl-shift-n": "agent::RejectAll",
|
"ctrl-shift-n": "agent::RejectAll",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -267,6 +267,7 @@
|
|||||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||||
"cmd-k l": "agent::OpenRulesLibrary",
|
"cmd-k l": "agent::OpenRulesLibrary",
|
||||||
"alt-tab": "agent::CycleFavoriteModels",
|
"alt-tab": "agent::CycleFavoriteModels",
|
||||||
|
"cmd-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -335,6 +336,7 @@
|
|||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"cmd-shift-y": "agent::KeepAll",
|
"cmd-shift-y": "agent::KeepAll",
|
||||||
"cmd-shift-n": "agent::RejectAll",
|
"cmd-shift-n": "agent::RejectAll",
|
||||||
|
"cmd-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -347,6 +349,7 @@
|
|||||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||||
"cmd-shift-y": "agent::KeepAll",
|
"cmd-shift-y": "agent::KeepAll",
|
||||||
"cmd-shift-n": "agent::RejectAll",
|
"cmd-shift-n": "agent::RejectAll",
|
||||||
|
"cmd-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -227,6 +227,7 @@
|
|||||||
"ctrl-g": "search::SelectNextMatch",
|
"ctrl-g": "search::SelectNextMatch",
|
||||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -296,6 +297,7 @@
|
|||||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
"ctrl-shift-y": "agent::KeepAll",
|
||||||
"ctrl-shift-n": "agent::RejectAll",
|
"ctrl-shift-n": "agent::RejectAll",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -308,6 +310,7 @@
|
|||||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||||
"ctrl-shift-y": "agent::KeepAll",
|
"ctrl-shift-y": "agent::KeepAll",
|
||||||
"ctrl-shift-n": "agent::RejectAll",
|
"ctrl-shift-n": "agent::RejectAll",
|
||||||
|
"ctrl-shift-v": "agent::PasteRaw",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ pub struct ToolCall {
|
|||||||
pub locations: Vec<acp::ToolCallLocation>,
|
pub locations: Vec<acp::ToolCallLocation>,
|
||||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||||
pub raw_input: Option<serde_json::Value>,
|
pub raw_input: Option<serde_json::Value>,
|
||||||
|
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||||
pub raw_output: Option<serde_json::Value>,
|
pub raw_output: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +223,11 @@ impl ToolCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let raw_input_markdown = tool_call
|
||||||
|
.raw_input
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||||
|
|
||||||
let result = Self {
|
let result = Self {
|
||||||
id: tool_call.tool_call_id,
|
id: tool_call.tool_call_id,
|
||||||
label: cx
|
label: cx
|
||||||
@@ -232,6 +238,7 @@ impl ToolCall {
|
|||||||
resolved_locations: Vec::default(),
|
resolved_locations: Vec::default(),
|
||||||
status,
|
status,
|
||||||
raw_input: tool_call.raw_input,
|
raw_input: tool_call.raw_input,
|
||||||
|
raw_input_markdown,
|
||||||
raw_output: tool_call.raw_output,
|
raw_output: tool_call.raw_output,
|
||||||
};
|
};
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -307,6 +314,7 @@ impl ToolCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(raw_input) = raw_input {
|
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);
|
self.raw_input = Some(raw_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1355,6 +1363,7 @@ impl AcpThread {
|
|||||||
locations: Vec::new(),
|
locations: Vec::new(),
|
||||||
resolved_locations: Vec::new(),
|
resolved_locations: Vec::new(),
|
||||||
raw_input: None,
|
raw_input: None,
|
||||||
|
raw_input_markdown: None,
|
||||||
raw_output: None,
|
raw_output: None,
|
||||||
};
|
};
|
||||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
|||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::{ResultExt, debug_panic};
|
use util::{ResultExt, debug_panic};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::Chat;
|
use zed_actions::agent::{Chat, PasteRaw};
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
mention_set: Entity<MentionSet>,
|
mention_set: Entity<MentionSet>,
|
||||||
@@ -543,6 +543,9 @@ impl MessageEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
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
|
let editor_clipboard_selections = cx
|
||||||
.read_from_clipboard()
|
.read_from_clipboard()
|
||||||
.and_then(|item| item.entries().first().cloned())
|
.and_then(|item| item.entries().first().cloned())
|
||||||
@@ -553,133 +556,127 @@ impl MessageEditor {
|
|||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let has_file_context = editor_clipboard_selections
|
// Insert creases for pasted clipboard selections that:
|
||||||
.as_ref()
|
// 1. Contain exactly one selection
|
||||||
.is_some_and(|selections| {
|
// 2. Have an associated file path
|
||||||
selections
|
// 3. Span multiple lines (not single-line selections)
|
||||||
.iter()
|
// 4. Belong to a file that exists in the current project
|
||||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
let should_insert_creases = util::maybe!({
|
||||||
});
|
let selections = editor_clipboard_selections.as_ref()?;
|
||||||
|
if selections.len() > 1 {
|
||||||
if has_file_context {
|
return Some(false);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
let selection = selections.first()?;
|
||||||
|
let file_path = selection.file_path.as_ref()?;
|
||||||
|
let line_range = selection.line_range.as_ref()?;
|
||||||
|
|
||||||
|
if line_range.start() == line_range.end() {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(file_path, cx)
|
||||||
|
.is_some(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||||
|
cx.stop_propagation();
|
||||||
|
let insertion_target = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.selections
|
||||||
|
.newest_anchor()
|
||||||
|
.start
|
||||||
|
.text_anchor;
|
||||||
|
|
||||||
|
let project = workspace.read(cx).project().clone();
|
||||||
|
for selection in selections {
|
||||||
|
if let (Some(file_path), Some(line_range)) =
|
||||||
|
(selection.file_path, selection.line_range)
|
||||||
|
{
|
||||||
|
let crease_text =
|
||||||
|
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||||
|
|
||||||
|
let mention_uri = MentionUri::Selection {
|
||||||
|
abs_path: Some(file_path.clone()),
|
||||||
|
line_range: line_range.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mention_text = mention_uri.as_link().to_string();
|
||||||
|
let (excerpt_id, text_anchor, content_len) =
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
let buffer = editor.buffer().read(cx);
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||||
|
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||||
|
|
||||||
|
editor.insert(&mention_text, window, cx);
|
||||||
|
editor.insert(" ", window, cx);
|
||||||
|
|
||||||
|
(*excerpt_id, text_anchor, mention_text.len())
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||||
|
excerpt_id,
|
||||||
|
text_anchor,
|
||||||
|
content_len,
|
||||||
|
crease_text.into(),
|
||||||
|
mention_uri.icon_path(cx),
|
||||||
|
None,
|
||||||
|
self.editor.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
drop(tx);
|
||||||
|
|
||||||
|
let mention_task = cx
|
||||||
|
.spawn({
|
||||||
|
let project = project.clone();
|
||||||
|
async move |_, cx| {
|
||||||
|
let project_path = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.project_path_for_absolute_path(&file_path, cx)
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.ok_or_else(|| "project path not found".to_string())?;
|
||||||
|
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
buffer
|
||||||
|
.update(cx, |buffer, cx| {
|
||||||
|
let start = Point::new(*line_range.start(), 0)
|
||||||
|
.min(buffer.max_point());
|
||||||
|
let end = Point::new(*line_range.end() + 1, 0)
|
||||||
|
.min(buffer.max_point());
|
||||||
|
let content = buffer.text_for_range(start..end).collect();
|
||||||
|
Mention::Text {
|
||||||
|
content,
|
||||||
|
tracked_buffers: vec![cx.entity()],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
|
||||||
|
self.mention_set.update(cx, |mention_set, _cx| {
|
||||||
|
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.prompt_capabilities.borrow().image
|
if self.prompt_capabilities.borrow().image
|
||||||
@@ -690,6 +687,13 @@ impl MessageEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
window.defer(cx, move |window, cx| {
|
||||||
|
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_dragged_files(
|
pub fn insert_dragged_files(
|
||||||
&mut self,
|
&mut self,
|
||||||
paths: Vec<project::ProjectPath>,
|
paths: Vec<project::ProjectPath>,
|
||||||
@@ -967,6 +971,7 @@ impl Render for MessageEditor {
|
|||||||
.on_action(cx.listener(Self::chat))
|
.on_action(cx.listener(Self::chat))
|
||||||
.on_action(cx.listener(Self::chat_with_follow))
|
.on_action(cx.listener(Self::chat_with_follow))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.on_action(cx.listener(Self::paste_raw))
|
||||||
.capture_action(cx.listener(Self::paste))
|
.capture_action(cx.listener(Self::paste))
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.child({
|
.child({
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
cx: &mut Context<Picker<Self>>,
|
cx: &mut Context<Picker<Self>>,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let favorites = if self.selector.supports_favorites() {
|
let favorites = if self.selector.supports_favorites() {
|
||||||
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
|
AgentSettings::get_global(cx).favorite_model_ids()
|
||||||
} else {
|
} else {
|
||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.delegate.filtered_entries =
|
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
|
// Finds the currently selected model in the list
|
||||||
let new_index = this
|
let new_index = this
|
||||||
.delegate
|
.delegate
|
||||||
@@ -406,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
|||||||
|
|
||||||
fn info_list_to_picker_entries(
|
fn info_list_to_picker_entries(
|
||||||
model_list: AgentModelList,
|
model_list: AgentModelList,
|
||||||
favorites: Arc<HashSet<ModelId>>,
|
favorites: &HashSet<ModelId>,
|
||||||
) -> Vec<AcpModelPickerEntry> {
|
) -> Vec<AcpModelPickerEntry> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
@@ -572,13 +572,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||||
Arc::new(
|
models
|
||||||
models
|
.into_iter()
|
||||||
.into_iter()
|
.map(|m| ModelId::new(m.to_string()))
|
||||||
.map(|m| ModelId::new(m.to_string()))
|
.collect()
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||||
@@ -609,7 +607,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
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!(
|
assert!(matches!(
|
||||||
entries.first(),
|
entries.first(),
|
||||||
@@ -625,7 +623,7 @@ mod tests {
|
|||||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||||
let favorites = create_favorites(vec![]);
|
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!(
|
assert!(matches!(
|
||||||
entries.first(),
|
entries.first(),
|
||||||
@@ -641,7 +639,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
let favorites = create_favorites(vec!["zed/claude"]);
|
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 {
|
for entry in &entries {
|
||||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||||
@@ -662,7 +660,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
let 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);
|
let model_ids = get_entry_model_ids(&entries);
|
||||||
|
|
||||||
assert_eq!(model_ids[0], "zed/gemini");
|
assert_eq!(model_ids[0], "zed/gemini");
|
||||||
@@ -683,7 +681,7 @@ mod tests {
|
|||||||
|
|
||||||
let favorites = create_favorites(vec!["zed/claude"]);
|
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);
|
let labels = get_entry_labels(&entries);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -723,7 +721,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
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!(
|
assert!(matches!(
|
||||||
entries.first(),
|
entries.first(),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ use language::Buffer;
|
|||||||
|
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use project::{Project, ProjectEntryId};
|
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
use prompt_store::{PromptId, PromptStore};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||||
@@ -260,6 +260,7 @@ impl ThreadFeedbackState {
|
|||||||
|
|
||||||
pub struct AcpThreadView {
|
pub struct AcpThreadView {
|
||||||
agent: Rc<dyn AgentServer>,
|
agent: Rc<dyn AgentServer>,
|
||||||
|
agent_server_store: Entity<AgentServerStore>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_state: ThreadState,
|
thread_state: ThreadState,
|
||||||
@@ -406,6 +407,7 @@ impl AcpThreadView {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
|
agent_server_store,
|
||||||
workspace: workspace.clone(),
|
workspace: workspace.clone(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
entry_view_state,
|
entry_view_state,
|
||||||
@@ -737,7 +739,7 @@ impl AcpThreadView {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let agent_name = agent.name();
|
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 registry = LanguageModelRegistry::global(cx);
|
||||||
|
|
||||||
let sub = window.subscribe(®istry, cx, {
|
let sub = window.subscribe(®istry, cx, {
|
||||||
@@ -779,7 +781,6 @@ impl AcpThreadView {
|
|||||||
configuration_view,
|
configuration_view,
|
||||||
description: err
|
description: err
|
||||||
.description
|
.description
|
||||||
.clone()
|
|
||||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
};
|
};
|
||||||
@@ -1088,10 +1089,7 @@ impl AcpThreadView {
|
|||||||
window.defer(cx, |window, cx| {
|
window.defer(cx, |window, cx| {
|
||||||
Self::handle_auth_required(
|
Self::handle_auth_required(
|
||||||
this,
|
this,
|
||||||
AuthRequired {
|
AuthRequired::new(),
|
||||||
description: None,
|
|
||||||
provider_id: None,
|
|
||||||
},
|
|
||||||
agent,
|
agent,
|
||||||
connection,
|
connection,
|
||||||
window,
|
window,
|
||||||
@@ -1663,44 +1661,6 @@ impl AcpThreadView {
|
|||||||
});
|
});
|
||||||
return;
|
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"
|
} else if method.0.as_ref() == "vertex-ai"
|
||||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||||
@@ -2153,6 +2113,7 @@ impl AcpThreadView {
|
|||||||
chunks,
|
chunks,
|
||||||
indented: _,
|
indented: _,
|
||||||
}) => {
|
}) => {
|
||||||
|
let mut is_blank = true;
|
||||||
let is_last = entry_ix + 1 == total_entries;
|
let is_last = entry_ix + 1 == total_entries;
|
||||||
|
|
||||||
let style = default_markdown_style(false, false, window, cx);
|
let style = default_markdown_style(false, false, window, cx);
|
||||||
@@ -2162,36 +2123,55 @@ impl AcpThreadView {
|
|||||||
.children(chunks.iter().enumerate().filter_map(
|
.children(chunks.iter().enumerate().filter_map(
|
||||||
|(chunk_ix, chunk)| match chunk {
|
|(chunk_ix, chunk)| match chunk {
|
||||||
AssistantMessageChunk::Message { block } => {
|
AssistantMessageChunk::Message { block } => {
|
||||||
block.markdown().map(|md| {
|
block.markdown().and_then(|md| {
|
||||||
self.render_markdown(md.clone(), style.clone())
|
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||||
.into_any_element()
|
is_blank = is_blank && this_is_blank;
|
||||||
|
if this_is_blank {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
self.render_markdown(md.clone(), style.clone())
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AssistantMessageChunk::Thought { block } => {
|
AssistantMessageChunk::Thought { block } => {
|
||||||
block.markdown().map(|md| {
|
block.markdown().and_then(|md| {
|
||||||
self.render_thinking_block(
|
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||||
entry_ix,
|
is_blank = is_blank && this_is_blank;
|
||||||
chunk_ix,
|
if this_is_blank {
|
||||||
md.clone(),
|
return None;
|
||||||
window,
|
}
|
||||||
cx,
|
|
||||||
|
Some(
|
||||||
|
self.render_thinking_block(
|
||||||
|
entry_ix,
|
||||||
|
chunk_ix,
|
||||||
|
md.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.into_any();
|
.into_any();
|
||||||
|
|
||||||
v_flex()
|
if is_blank {
|
||||||
.px_5()
|
Empty.into_any()
|
||||||
.py_1p5()
|
} else {
|
||||||
.when(is_first_indented, |this| this.pt_0p5())
|
v_flex()
|
||||||
.when(is_last, |this| this.pb_4())
|
.px_5()
|
||||||
.w_full()
|
.py_1p5()
|
||||||
.text_ui(cx)
|
.when(is_last, |this| this.pb_4())
|
||||||
.child(message_body)
|
.w_full()
|
||||||
.into_any()
|
.text_ui(cx)
|
||||||
|
.child(message_body)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AgentThreadEntry::ToolCall(tool_call) => {
|
AgentThreadEntry::ToolCall(tool_call) => {
|
||||||
let has_terminals = tool_call.terminals().next().is_some();
|
let has_terminals = tool_call.terminals().next().is_some();
|
||||||
@@ -2223,7 +2203,7 @@ impl AcpThreadView {
|
|||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.pl(rems_from_px(20.0))
|
.pl_5()
|
||||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -2440,6 +2420,12 @@ impl AcpThreadView {
|
|||||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||||
|
|
||||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
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 =
|
let tool_output_display =
|
||||||
if is_open {
|
if is_open {
|
||||||
@@ -2481,7 +2467,25 @@ impl AcpThreadView {
|
|||||||
| ToolCallStatus::Completed
|
| ToolCallStatus::Completed
|
||||||
| ToolCallStatus::Failed
|
| ToolCallStatus::Failed
|
||||||
| ToolCallStatus::Canceled => v_flex()
|
| ToolCallStatus::Canceled => v_flex()
|
||||||
.w_full()
|
.when(!is_edit && !is_terminal_tool, |this| {
|
||||||
|
this.mt_1p5().w_full().child(
|
||||||
|
v_flex()
|
||||||
|
.ml(rems(0.4))
|
||||||
|
.px_3p5()
|
||||||
|
.pb_1()
|
||||||
|
.gap_1()
|
||||||
|
.border_l_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
|
.child(input_output_header("Raw Input:".into()))
|
||||||
|
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||||
|
self.render_markdown(
|
||||||
|
input,
|
||||||
|
default_markdown_style(false, false, window, cx),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
.child(input_output_header("Output:".into())),
|
||||||
|
)
|
||||||
|
})
|
||||||
.children(tool_call.content.iter().enumerate().map(
|
.children(tool_call.content.iter().enumerate().map(
|
||||||
|(content_ix, content)| {
|
|(content_ix, content)| {
|
||||||
div().child(self.render_tool_call_content(
|
div().child(self.render_tool_call_content(
|
||||||
@@ -2580,7 +2584,7 @@ impl AcpThreadView {
|
|||||||
.gap_px()
|
.gap_px()
|
||||||
.when(is_collapsible, |this| {
|
.when(is_collapsible, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||||
.opened_icon(IconName::ChevronUp)
|
.opened_icon(IconName::ChevronUp)
|
||||||
.closed_icon(IconName::ChevronDown)
|
.closed_icon(IconName::ChevronDown)
|
||||||
.visible_on_hover(&card_header_id)
|
.visible_on_hover(&card_header_id)
|
||||||
@@ -2766,20 +2770,20 @@ impl AcpThreadView {
|
|||||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_1p5()
|
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when(!card_layout, |this| {
|
.map(|this| {
|
||||||
this.ml(rems(0.4))
|
if card_layout {
|
||||||
.px_3p5()
|
this.when(context_ix > 0, |this| {
|
||||||
.border_l_1()
|
this.pt_2()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_t_1()
|
||||||
})
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.when(card_layout, |this| {
|
})
|
||||||
this.px_2().pb_2().when(context_ix > 0, |this| {
|
} else {
|
||||||
this.border_t_1()
|
this.ml(rems(0.4))
|
||||||
.pt_2()
|
.px_3p5()
|
||||||
|
.border_l_1()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_color(self.tool_card_border_color(cx))
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
@@ -3500,138 +3504,119 @@ impl AcpThreadView {
|
|||||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> impl IntoElement {
|
||||||
let show_description =
|
|
||||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
|
||||||
|
|
||||||
let auth_methods = connection.auth_methods();
|
let auth_methods = connection.auth_methods();
|
||||||
|
|
||||||
v_flex().flex_1().size_full().justify_end().child(
|
let agent_display_name = self
|
||||||
v_flex()
|
.agent_server_store
|
||||||
.p_2()
|
.read(cx)
|
||||||
.pr_3()
|
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||||
.w_full()
|
.unwrap_or_else(|| self.agent.name());
|
||||||
.gap_1()
|
|
||||||
.border_t_1()
|
let show_fallback_description = auth_methods.len() > 1
|
||||||
.border_color(cx.theme().colors().border)
|
&& configuration_view.is_none()
|
||||||
.bg(cx.theme().status().warning.opacity(0.04))
|
&& description.is_none()
|
||||||
.child(
|
&& pending_auth_method.is_none();
|
||||||
h_flex()
|
|
||||||
.gap_1p5()
|
let auth_buttons = || {
|
||||||
.child(
|
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||||
Icon::new(IconName::Warning)
|
connection
|
||||||
.color(Color::Warning)
|
.auth_methods()
|
||||||
.size(IconSize::Small),
|
.iter()
|
||||||
)
|
.enumerate()
|
||||||
.child(Label::new("Authentication Required").size(LabelSize::Small)),
|
.rev()
|
||||||
)
|
.map(|(ix, method)| {
|
||||||
.children(description.map(|desc| {
|
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
|
||||||
div().text_ui(cx).child(self.render_markdown(
|
&& method.id.0.as_ref() == "oauth-personal"
|
||||||
desc.clone(),
|
&& method.name == "Log in with Google"
|
||||||
default_markdown_style(false, false, window, cx),
|
{
|
||||||
))
|
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||||
}))
|
} else {
|
||||||
.children(
|
(method.id.0.clone(), method.name.clone())
|
||||||
configuration_view
|
};
|
||||||
.cloned()
|
|
||||||
.map(|view| div().w_full().child(view)),
|
let agent_telemetry_id = connection.telemetry_id();
|
||||||
)
|
|
||||||
.when(show_description, |el| {
|
Button::new(method_id.clone(), name)
|
||||||
el.child(
|
.label_size(LabelSize::Small)
|
||||||
Label::new(format!(
|
.map(|this| {
|
||||||
"You are not currently authenticated with {}.{}",
|
if ix == 0 {
|
||||||
self.agent.name(),
|
this.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
if auth_methods.len() > 1 {
|
} else {
|
||||||
" Please choose one of the following options:"
|
this.style(ButtonStyle::Outlined)
|
||||||
} 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(
|
.when_some(method.description.clone(), |this, description| {
|
||||||
|(ix, method)| {
|
this.tooltip(Tooltip::text(description))
|
||||||
let (method_id, name) = if self
|
})
|
||||||
.project
|
.on_click({
|
||||||
.read(cx)
|
cx.listener(move |this, _, window, cx| {
|
||||||
.is_via_remote_server()
|
telemetry::event!(
|
||||||
&& method.id.0.as_ref() == "oauth-personal"
|
"Authenticate Agent Started",
|
||||||
&& method.name == "Log in with Google"
|
agent = agent_telemetry_id,
|
||||||
{
|
method = method_id
|
||||||
("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();
|
this.authenticate(
|
||||||
|
acp::AuthMethodId::new(method_id.clone()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
Button::new(method_id.clone(), name)
|
if pending_auth_method.is_some() {
|
||||||
.label_size(LabelSize::Small)
|
return Callout::new()
|
||||||
.map(|this| {
|
.icon(IconName::Info)
|
||||||
if ix == 0 {
|
.title(format!("Authenticating to {}…", agent_display_name))
|
||||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
.actions_slot(
|
||||||
} else {
|
Icon::new(IconName::ArrowCircle)
|
||||||
this.style(ButtonStyle::Outlined)
|
.size(IconSize::Small)
|
||||||
}
|
.color(Color::Muted)
|
||||||
})
|
.with_rotate_animation(2)
|
||||||
.when_some(
|
.into_any_element(),
|
||||||
method.description.clone(),
|
)
|
||||||
|this, description| {
|
.into_any_element();
|
||||||
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(
|
Callout::new()
|
||||||
acp::AuthMethodId::new(method_id.clone()),
|
.icon(IconName::Info)
|
||||||
window,
|
.title(format!("Authenticate to {}", agent_display_name))
|
||||||
cx,
|
.when(auth_methods.len() == 1, |this| {
|
||||||
)
|
this.actions_slot(auth_buttons())
|
||||||
})
|
})
|
||||||
})
|
.description_slot(
|
||||||
},
|
v_flex()
|
||||||
)),
|
.text_ui(cx)
|
||||||
)
|
.map(|this| {
|
||||||
}),
|
if show_fallback_description {
|
||||||
)
|
this.child(
|
||||||
|
Label::new("Choose one of the following authentication options:")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.children(
|
||||||
|
configuration_view
|
||||||
|
.cloned()
|
||||||
|
.map(|view| div().w_full().child(view)),
|
||||||
|
)
|
||||||
|
.children(description.map(|desc| {
|
||||||
|
self.render_markdown(
|
||||||
|
desc.clone(),
|
||||||
|
default_markdown_style(false, false, window, cx),
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.when(auth_methods.len() > 1, |this| {
|
||||||
|
this.gap_1().child(auth_buttons())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_load_error(
|
fn render_load_error(
|
||||||
@@ -5880,10 +5865,6 @@ impl AcpThreadView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let connection = thread.read(cx).connection().clone();
|
let connection = thread.read(cx).connection().clone();
|
||||||
let err = AuthRequired {
|
|
||||||
description: None,
|
|
||||||
provider_id: None,
|
|
||||||
};
|
|
||||||
this.clear_thread_error(cx);
|
this.clear_thread_error(cx);
|
||||||
if let Some(message) = this.in_flight_prompt.take() {
|
if let Some(message) = this.in_flight_prompt.take() {
|
||||||
this.message_editor.update(cx, |editor, cx| {
|
this.message_editor.update(cx, |editor, cx| {
|
||||||
@@ -5892,7 +5873,14 @@ impl AcpThreadView {
|
|||||||
}
|
}
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
window.defer(cx, |window, cx| {
|
window.defer(cx, |window, cx| {
|
||||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
Self::handle_auth_required(
|
||||||
|
this,
|
||||||
|
AuthRequired::new(),
|
||||||
|
agent,
|
||||||
|
connection,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -5905,14 +5893,10 @@ impl AcpThreadView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let connection = thread.read(cx).connection().clone();
|
let connection = thread.read(cx).connection().clone();
|
||||||
let err = AuthRequired {
|
|
||||||
description: None,
|
|
||||||
provider_id: None,
|
|
||||||
};
|
|
||||||
self.clear_thread_error(cx);
|
self.clear_thread_error(cx);
|
||||||
let this = cx.weak_entity();
|
let this = cx.weak_entity();
|
||||||
window.defer(cx, |window, cx| {
|
window.defer(cx, |window, cx| {
|
||||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6015,16 +5999,19 @@ impl Render for AcpThreadView {
|
|||||||
configuration_view,
|
configuration_view,
|
||||||
pending_auth_method,
|
pending_auth_method,
|
||||||
..
|
..
|
||||||
} => self
|
} => v_flex()
|
||||||
.render_auth_required_state(
|
.flex_1()
|
||||||
|
.size_full()
|
||||||
|
.justify_end()
|
||||||
|
.child(self.render_auth_required_state(
|
||||||
connection,
|
connection,
|
||||||
description.as_ref(),
|
description.as_ref(),
|
||||||
configuration_view.as_ref(),
|
configuration_view.as_ref(),
|
||||||
pending_auth_method.as_ref(),
|
pending_auth_method.as_ref(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
))
|
||||||
.into_any(),
|
.into_any_element(),
|
||||||
ThreadState::Loading { .. } => v_flex()
|
ThreadState::Loading { .. } => v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.child(self.render_recent_history(cx))
|
.child(self.render_recent_history(cx))
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ use workspace::{
|
|||||||
pane,
|
pane,
|
||||||
searchable::{SearchEvent, SearchableItem},
|
searchable::{SearchEvent, SearchableItem},
|
||||||
};
|
};
|
||||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||||
|
|
||||||
use crate::CycleFavoriteModels;
|
use crate::CycleFavoriteModels;
|
||||||
|
|
||||||
@@ -1698,6 +1698,9 @@ impl TextThreadEditor {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let editor_clipboard_selections = cx
|
let editor_clipboard_selections = cx
|
||||||
.read_from_clipboard()
|
.read_from_clipboard()
|
||||||
.and_then(|item| item.entries().first().cloned())
|
.and_then(|item| item.entries().first().cloned())
|
||||||
@@ -1708,84 +1711,101 @@ impl TextThreadEditor {
|
|||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let has_file_context = editor_clipboard_selections
|
// Insert creases for pasted clipboard selections that:
|
||||||
.as_ref()
|
// 1. Contain exactly one selection
|
||||||
.is_some_and(|selections| {
|
// 2. Have an associated file path
|
||||||
selections
|
// 3. Span multiple lines (not single-line selections)
|
||||||
.iter()
|
// 4. Belong to a file that exists in the current project
|
||||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
let should_insert_creases = util::maybe!({
|
||||||
});
|
let selections = editor_clipboard_selections.as_ref()?;
|
||||||
|
if selections.len() > 1 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let selection = selections.first()?;
|
||||||
|
let file_path = selection.file_path.as_ref()?;
|
||||||
|
let line_range = selection.line_range.as_ref()?;
|
||||||
|
|
||||||
if has_file_context {
|
if line_range.start() == line_range.end() {
|
||||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
return Some(false);
|
||||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
}
|
||||||
clipboard_item.entries().first()
|
|
||||||
{
|
|
||||||
if let Some(selections) = editor_clipboard_selections {
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
let text = clipboard_text.text();
|
Some(
|
||||||
self.editor.update(cx, |editor, cx| {
|
workspace
|
||||||
let mut current_offset = 0;
|
.read(cx)
|
||||||
let weak_editor = cx.entity().downgrade();
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.project_path_for_absolute_path(file_path, cx)
|
||||||
|
.is_some(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
for selection in selections {
|
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||||
if let (Some(file_path), Some(line_range)) =
|
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||||
(selection.file_path, selection.line_range)
|
if let Some(selections) = editor_clipboard_selections {
|
||||||
{
|
cx.stop_propagation();
|
||||||
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 insert_point = editor
|
let text = clipboard_text.text();
|
||||||
.selections
|
self.editor.update(cx, |editor, cx| {
|
||||||
.newest::<Point>(&editor.display_snapshot(cx))
|
let mut current_offset = 0;
|
||||||
.head();
|
let weak_editor = cx.entity().downgrade();
|
||||||
let start_row = MultiBufferRow(insert_point.row);
|
|
||||||
|
|
||||||
editor.insert(&formatted_text, window, cx);
|
for selection in selections {
|
||||||
|
if let (Some(file_path), Some(line_range)) =
|
||||||
|
(selection.file_path, selection.line_range)
|
||||||
|
{
|
||||||
|
let selected_text =
|
||||||
|
&text[current_offset..current_offset + selection.len];
|
||||||
|
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||||
|
file_path.to_str(),
|
||||||
|
Some(line_range.clone()),
|
||||||
|
);
|
||||||
|
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||||
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let insert_point = editor
|
||||||
let anchor_before = snapshot.anchor_after(insert_point);
|
.selections
|
||||||
let anchor_after = editor
|
.newest::<Point>(&editor.display_snapshot(cx))
|
||||||
.selections
|
.head();
|
||||||
.newest_anchor()
|
let start_row = MultiBufferRow(insert_point.row);
|
||||||
.head()
|
|
||||||
.bias_left(&snapshot);
|
|
||||||
|
|
||||||
editor.insert("\n", window, cx);
|
editor.insert(&formatted_text, window, cx);
|
||||||
|
|
||||||
let crease_text = acp_thread::selection_name(
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
Some(file_path.as_ref()),
|
let anchor_before = snapshot.anchor_after(insert_point);
|
||||||
&line_range,
|
let anchor_after = editor
|
||||||
);
|
.selections
|
||||||
|
.newest_anchor()
|
||||||
|
.head()
|
||||||
|
.bias_left(&snapshot);
|
||||||
|
|
||||||
let fold_placeholder = quote_selection_fold_placeholder(
|
editor.insert("\n", window, cx);
|
||||||
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;
|
let crease_text = acp_thread::selection_name(
|
||||||
if !selection.is_entire_line && current_offset < text.len() {
|
Some(file_path.as_ref()),
|
||||||
current_offset += 1;
|
&line_range,
|
||||||
}
|
);
|
||||||
|
|
||||||
|
let fold_placeholder = quote_selection_fold_placeholder(
|
||||||
|
crease_text,
|
||||||
|
weak_editor.clone(),
|
||||||
|
);
|
||||||
|
let crease = Crease::inline(
|
||||||
|
anchor_before..anchor_after,
|
||||||
|
fold_placeholder,
|
||||||
|
render_quote_selection_output_toggle,
|
||||||
|
|_, _, _, _| Empty.into_any(),
|
||||||
|
);
|
||||||
|
editor.insert_creases(vec![crease], cx);
|
||||||
|
editor.fold_at(start_row, window, cx);
|
||||||
|
|
||||||
|
current_offset += selection.len;
|
||||||
|
if !selection.is_entire_line && current_offset < text.len() {
|
||||||
|
current_offset += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return;
|
});
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1944,6 +1964,12 @@ impl TextThreadEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.paste(&editor::actions::Paste, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
@@ -2627,6 +2653,7 @@ impl Render for TextThreadEditor {
|
|||||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
.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::cycle_message_role))
|
||||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||||
.on_action(cx.listener(TextThreadEditor::assist))
|
.on_action(cx.listener(TextThreadEditor::assist))
|
||||||
|
|||||||
@@ -103,8 +103,9 @@ impl Model {
|
|||||||
|
|
||||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||||
match self {
|
match self {
|
||||||
Self::Chat => Some(8_192),
|
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||||
Self::Reasoner => Some(64_000),
|
// Using the default value of None in the API instead
|
||||||
|
Self::Chat | Self::Reasoner => None,
|
||||||
Self::Custom {
|
Self::Custom {
|
||||||
max_output_tokens, ..
|
max_output_tokens, ..
|
||||||
} => *max_output_tokens,
|
} => *max_output_tokens,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use futures::AsyncReadExt as _;
|
use futures::AsyncReadExt as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Entity, SharedString, Task,
|
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||||
http_client::{self, AsyncBody, Method},
|
http_client::{self, AsyncBody, Method},
|
||||||
};
|
};
|
||||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||||
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
|||||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
|
||||||
|
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||||
|
|
||||||
|
impl Global for GlobalMercuryApiKey {}
|
||||||
|
|
||||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||||
MERCURY_API_KEY
|
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||||
.get_or_init(|| {
|
return global.0.clone();
|
||||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
}
|
||||||
})
|
let entity =
|
||||||
.clone()
|
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||||
|
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||||
|
entity
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::AsyncReadExt as _;
|
use futures::AsyncReadExt as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Entity, SharedString, Task,
|
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||||
http_client::{self, AsyncBody, Method},
|
http_client::{self, AsyncBody, Method},
|
||||||
};
|
};
|
||||||
use language::{Point, ToOffset as _};
|
use language::{Point, ToOffset as _};
|
||||||
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
|||||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
|
||||||
|
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||||
|
|
||||||
|
impl Global for GlobalSweepApiKey {}
|
||||||
|
|
||||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||||
SWEEP_API_KEY
|
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||||
.get_or_init(|| {
|
return global.0.clone();
|
||||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
}
|
||||||
})
|
let entity =
|
||||||
.clone()
|
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||||
|
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||||
|
entity
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||||
|
|||||||
@@ -348,6 +348,61 @@ where
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |language_settings| {
|
||||||
|
language_settings.defaults.colorize_brackets = Some(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||||
|
language_registry.add(markdown_lang());
|
||||||
|
language_registry.add(rust_lang());
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
cx.update_buffer(|buffer, cx| {
|
||||||
|
buffer.set_language_registry(language_registry.clone());
|
||||||
|
buffer.set_language(Some(markdown_lang()), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.set_state(indoc! {r#"
|
||||||
|
fn main() {
|
||||||
|
let v: Vec<Stringˇ> = vec![];
|
||||||
|
}
|
||||||
|
"#});
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(100));
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"fn main«1()1» «1{
|
||||||
|
let v: Vec<String> = vec!«2[]2»;
|
||||||
|
}1»
|
||||||
|
|
||||||
|
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||||
|
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||||
|
"#,
|
||||||
|
&bracket_colors_markup(&mut cx),
|
||||||
|
"Markdown does not colorize <> brackets"
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_buffer(|buffer, cx| {
|
||||||
|
buffer.set_language(Some(rust_lang()), cx);
|
||||||
|
});
|
||||||
|
cx.executor().advance_clock(Duration::from_millis(100));
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"fn main«1()1» «1{
|
||||||
|
let v: Vec«2<String>2» = vec!«2[]2»;
|
||||||
|
}1»
|
||||||
|
|
||||||
|
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||||
|
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||||
|
"#,
|
||||||
|
&bracket_colors_markup(&mut cx),
|
||||||
|
"After switching to Rust, <> brackets are now colorized"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |language_settings| {
|
init_test(cx, |language_settings| {
|
||||||
|
|||||||
@@ -20880,6 +20880,36 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
|||||||
.to_string(),
|
.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! { "
|
cx.set_state(indoc! { "
|
||||||
one
|
one
|
||||||
ˇTWO
|
ˇTWO
|
||||||
@@ -20919,6 +20949,66 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_toggling_adjacent_diff_hunks_2(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
|
||||||
|
let diff_base = r#"
|
||||||
|
lineA
|
||||||
|
lineB
|
||||||
|
lineC
|
||||||
|
lineD
|
||||||
|
"#
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
&r#"
|
||||||
|
ˇlineA1
|
||||||
|
lineB
|
||||||
|
lineD
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
cx.set_head_text(&diff_base);
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||||
|
});
|
||||||
|
executor.run_until_parked();
|
||||||
|
cx.assert_state_with_diff(
|
||||||
|
r#"
|
||||||
|
- lineA
|
||||||
|
+ ˇlineA1
|
||||||
|
lineB
|
||||||
|
lineD
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.move_down(&MoveDown, window, cx);
|
||||||
|
editor.move_right(&MoveRight, window, cx);
|
||||||
|
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||||
|
});
|
||||||
|
executor.run_until_parked();
|
||||||
|
cx.assert_state_with_diff(
|
||||||
|
r#"
|
||||||
|
- lineA
|
||||||
|
+ lineA1
|
||||||
|
lˇineB
|
||||||
|
- lineC
|
||||||
|
lineD
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_edits_around_expanded_deletion_hunks(
|
async fn test_edits_around_expanded_deletion_hunks(
|
||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
|
|||||||
@@ -331,7 +331,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
|||||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||||
|
|
||||||
mod wit {
|
mod wit {
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
skip: ["init-extension"],
|
skip: ["init-extension"],
|
||||||
path: "./wit/since_v0.8.0",
|
path: "./wit/since_v0.8.0",
|
||||||
@@ -524,6 +523,12 @@ impl wit::Guest for Component {
|
|||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
pub struct LanguageServerId(String);
|
pub struct LanguageServerId(String);
|
||||||
|
|
||||||
|
impl LanguageServerId {
|
||||||
|
pub fn new(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<str> for LanguageServerId {
|
impl AsRef<str> for LanguageServerId {
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
&self.0
|
&self.0
|
||||||
@@ -540,6 +545,12 @@ impl fmt::Display for LanguageServerId {
|
|||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
pub struct ContextServerId(String);
|
pub struct ContextServerId(String);
|
||||||
|
|
||||||
|
impl ContextServerId {
|
||||||
|
pub fn new(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<str> for ContextServerId {
|
impl AsRef<str> for ContextServerId {
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
&self.0
|
&self.0
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
use gpui::{App, Context, WeakEntity, Window};
|
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use ui::{Color, IconName, SharedString};
|
|
||||||
use util::ResultExt;
|
|
||||||
use workspace::{self, Workspace};
|
|
||||||
|
|
||||||
pub fn clone_and_open(
|
|
||||||
repo_url: SharedString,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
on_success: Arc<
|
|
||||||
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
|
||||||
files: false,
|
|
||||||
directories: true,
|
|
||||||
multiple: false,
|
|
||||||
prompt: Some("Select as Repository Destination".into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
window
|
|
||||||
.spawn(cx, async move |cx| {
|
|
||||||
let mut paths = destination_prompt.await.ok()?.ok()??;
|
|
||||||
let mut destination_dir = paths.pop()?;
|
|
||||||
|
|
||||||
let repo_name = repo_url
|
|
||||||
.split('/')
|
|
||||||
.next_back()
|
|
||||||
.map(|name| name.strip_suffix(".git").unwrap_or(name))
|
|
||||||
.unwrap_or("repository")
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
let clone_task = workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
let fs = workspace.app_state().fs.clone();
|
|
||||||
let destination_dir = destination_dir.clone();
|
|
||||||
let repo_url = repo_url.clone();
|
|
||||||
cx.spawn(async move |_workspace, _cx| {
|
|
||||||
fs.git_clone(&repo_url, destination_dir.as_path()).await
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
if let Err(error) = clone_task.await {
|
|
||||||
workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
|
|
||||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
|
||||||
.dismiss_button(true)
|
|
||||||
});
|
|
||||||
workspace.toggle_status_toast(toast, cx);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_worktrees = workspace
|
|
||||||
.read_with(cx, |workspace, cx| {
|
|
||||||
workspace.project().read(cx).worktrees(cx).next().is_some()
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let prompt_answer = if has_worktrees {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.prompt(
|
|
||||||
gpui::PromptLevel::Info,
|
|
||||||
&format!("Git Clone: {}", repo_name),
|
|
||||||
None,
|
|
||||||
&["Add repo to project", "Open repo in new project"],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.ok()?
|
|
||||||
.await
|
|
||||||
.ok()?
|
|
||||||
} else {
|
|
||||||
// Don't ask if project is empty
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
destination_dir.push(&repo_name);
|
|
||||||
|
|
||||||
match prompt_answer {
|
|
||||||
0 => {
|
|
||||||
workspace
|
|
||||||
.update_in(cx, |workspace, window, cx| {
|
|
||||||
let create_task = workspace.project().update(cx, |project, cx| {
|
|
||||||
project.create_worktree(destination_dir.as_path(), true, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let workspace_weak = cx.weak_entity();
|
|
||||||
let on_success = on_success.clone();
|
|
||||||
cx.spawn_in(window, async move |_window, cx| {
|
|
||||||
if create_task.await.log_err().is_some() {
|
|
||||||
workspace_weak
|
|
||||||
.update_in(cx, |workspace, window, cx| {
|
|
||||||
(on_success)(workspace, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
workspace
|
|
||||||
.update(cx, move |workspace, cx| {
|
|
||||||
let app_state = workspace.app_state().clone();
|
|
||||||
let destination_path = destination_dir.clone();
|
|
||||||
let on_success = on_success.clone();
|
|
||||||
|
|
||||||
workspace::open_new(
|
|
||||||
Default::default(),
|
|
||||||
app_state,
|
|
||||||
cx,
|
|
||||||
move |workspace, window, cx| {
|
|
||||||
cx.activate(true);
|
|
||||||
|
|
||||||
let create_task =
|
|
||||||
workspace.project().update(cx, |project, cx| {
|
|
||||||
project.create_worktree(
|
|
||||||
destination_path.as_path(),
|
|
||||||
true,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let workspace_weak = cx.weak_entity();
|
|
||||||
cx.spawn_in(window, async move |_window, cx| {
|
|
||||||
if create_task.await.log_err().is_some() {
|
|
||||||
workspace_weak
|
|
||||||
.update_in(cx, |workspace, window, cx| {
|
|
||||||
(on_success)(workspace, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
@@ -2848,15 +2848,93 @@ impl GitPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select as Repository Destination".into()),
|
||||||
|
});
|
||||||
|
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
crate::clone::clone_and_open(
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
repo.into(),
|
let mut paths = path.await.ok()?.ok()??;
|
||||||
workspace,
|
let mut path = paths.pop()?;
|
||||||
window,
|
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
|
||||||
cx,
|
|
||||||
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
|
||||||
);
|
|
||||||
|
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
|
||||||
|
Ok(_) => cx.update(|window, cx| {
|
||||||
|
window.prompt(
|
||||||
|
PromptLevel::Info,
|
||||||
|
&format!("Git Clone: {}", repo_name),
|
||||||
|
None,
|
||||||
|
&["Add repo to project", "Open repo in new project"],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this: &mut GitPanel, cx| {
|
||||||
|
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
|
||||||
|
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||||
|
.dismiss_button(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.toggle_status_toast(toast, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
path.push(repo_name);
|
||||||
|
match prompt_answer.await.ok()? {
|
||||||
|
0 => {
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.create_worktree(path.as_path(), true, cx)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
workspace
|
||||||
|
.update(cx, move |workspace, cx| {
|
||||||
|
workspace::open_new(
|
||||||
|
Default::default(),
|
||||||
|
workspace.app_state().clone(),
|
||||||
|
cx,
|
||||||
|
move |workspace, _, cx| {
|
||||||
|
cx.activate(true);
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.create_worktree(&path, true, cx)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use ui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod blame_ui;
|
mod blame_ui;
|
||||||
pub mod clone;
|
|
||||||
|
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||||
|
|||||||
@@ -566,22 +566,22 @@ impl Model {
|
|||||||
|
|
||||||
pub fn max_token_count(&self) -> u64 {
|
pub fn max_token_count(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Gemini25FlashLite => 1_048_576,
|
Self::Gemini25FlashLite
|
||||||
Self::Gemini25Flash => 1_048_576,
|
| Self::Gemini25Flash
|
||||||
Self::Gemini25Pro => 1_048_576,
|
| Self::Gemini25Pro
|
||||||
Self::Gemini3Pro => 1_048_576,
|
| Self::Gemini3Pro
|
||||||
Self::Gemini3Flash => 1_048_576,
|
| Self::Gemini3Flash => 1_048_576,
|
||||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||||
match self {
|
match self {
|
||||||
Model::Gemini25FlashLite => Some(65_536),
|
Model::Gemini25FlashLite
|
||||||
Model::Gemini25Flash => Some(65_536),
|
| Model::Gemini25Flash
|
||||||
Model::Gemini25Pro => Some(65_536),
|
| Model::Gemini25Pro
|
||||||
Model::Gemini3Pro => Some(65_536),
|
| Model::Gemini3Pro
|
||||||
Model::Gemini3Flash => Some(65_536),
|
| Model::Gemini3Flash => Some(65_536),
|
||||||
Model::Custom { .. } => None,
|
Model::Custom { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
|
|||||||
"client_system",
|
"client_system",
|
||||||
"dlopen",
|
"dlopen",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
wayland-client = { version = "0.31.2", optional = true }
|
wayland-client = { version = "0.31.11", optional = true }
|
||||||
wayland-cursor = { version = "0.31.1", optional = true }
|
wayland-cursor = { version = "0.31.11", optional = true }
|
||||||
wayland-protocols = { version = "0.31.2", features = [
|
wayland-protocols = { version = "0.32.9", features = [
|
||||||
"client",
|
"client",
|
||||||
"staging",
|
"staging",
|
||||||
"unstable",
|
"unstable",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||||
"client",
|
"client",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use gpui::{
|
|||||||
|
|
||||||
struct SubWindow {
|
struct SubWindow {
|
||||||
custom_titlebar: bool,
|
custom_titlebar: bool,
|
||||||
|
is_dialog: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
|
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
|
||||||
@@ -23,7 +24,10 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for SubWindow {
|
impl Render for SubWindow {
|
||||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let window_bounds =
|
||||||
|
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
@@ -52,8 +56,28 @@ impl Render for SubWindow {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.p_8()
|
.p_8()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child("SubWindow")
|
.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, _| {
|
.child(button("Close", |window, _| {
|
||||||
window.remove_window();
|
window.remove_window();
|
||||||
})),
|
})),
|
||||||
@@ -86,6 +110,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -101,6 +126,39 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}))
|
||||||
|
.child(button("Floating", move |_, cx| {
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(window_bounds),
|
||||||
|
kind: WindowKind::Floating,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|_, cx| {
|
||||||
|
cx.new(|_| SubWindow {
|
||||||
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}))
|
||||||
|
.child(button("Dialog", move |_, cx| {
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
window_bounds: Some(window_bounds),
|
||||||
|
kind: WindowKind::Dialog,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|_, cx| {
|
||||||
|
cx.new(|_| SubWindow {
|
||||||
|
custom_titlebar: false,
|
||||||
|
is_dialog: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -116,6 +174,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: true,
|
custom_titlebar: true,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -131,6 +190,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -147,6 +207,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -162,6 +223,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -177,6 +239,7 @@ impl Render for WindowDemo {
|
|||||||
|_, cx| {
|
|_, cx| {
|
||||||
cx.new(|_| SubWindow {
|
cx.new(|_| SubWindow {
|
||||||
custom_titlebar: false,
|
custom_titlebar: false,
|
||||||
|
is_dialog: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ pub use entity_map::*;
|
|||||||
use http_client::{HttpClient, Url};
|
use http_client::{HttpClient, Url};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_app::*;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub use test_context::*;
|
pub use test_context::*;
|
||||||
use util::{ResultExt, debug_panic};
|
use util::{ResultExt, debug_panic};
|
||||||
|
|
||||||
@@ -51,6 +53,8 @@ mod async_context;
|
|||||||
mod context;
|
mod context;
|
||||||
mod entity_map;
|
mod entity_map;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
mod test_app;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
mod test_context;
|
mod test_context;
|
||||||
|
|
||||||
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.
|
||||||
|
|||||||
605
crates/gpui/src/app/test_app.rs
Normal file
605
crates/gpui/src/app/test_app.rs
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
//! 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,
|
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
||||||
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
||||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestPlatformWindow,
|
||||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
TestScreenCaptureSource, TextSystem, VisualContext, Window, WindowBounds, WindowHandle,
|
||||||
WindowHandle, WindowOptions, app::GpuiMode,
|
WindowOptions, app::GpuiMode,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use futures::{Stream, StreamExt, channel::oneshot};
|
use futures::{Stream, StreamExt, channel::oneshot};
|
||||||
@@ -220,7 +220,7 @@ impl TestAppContext {
|
|||||||
f(&cx)
|
f(&cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new window. The Window will always be backed by a `TestWindow` which
|
/// Adds a new window. The Window will always be backed by a `TestPlatformWindow` which
|
||||||
/// can be retrieved with `self.test_window(handle)`
|
/// can be retrieved with `self.test_window(handle)`
|
||||||
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
|
||||||
where
|
where
|
||||||
@@ -465,8 +465,8 @@ impl TestAppContext {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `TestWindow` backing the given handle.
|
/// Returns the `TestPlatformWindow` backing the given handle.
|
||||||
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
|
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestPlatformWindow {
|
||||||
self.app
|
self.app
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.windows
|
.windows
|
||||||
|
|||||||
@@ -808,6 +808,15 @@ impl LinearColorStop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Background {
|
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.
|
/// Use specified color space for color interpolation.
|
||||||
///
|
///
|
||||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
|
/// <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>);
|
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1348,6 +1348,10 @@ pub enum WindowKind {
|
|||||||
/// docks, notifications or wallpapers.
|
/// docks, notifications or wallpapers.
|
||||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||||
LayerShell(layer_shell::LayerShellOptions),
|
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.
|
/// The appearance of the window, as defined by the operating system.
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ use wayland_client::{
|
|||||||
wl_shm_pool, wl_surface,
|
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::{
|
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
|
||||||
self, ZwpPrimarySelectionOfferV1,
|
self, ZwpPrimarySelectionOfferV1,
|
||||||
};
|
};
|
||||||
@@ -61,6 +55,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
|||||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||||
};
|
};
|
||||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
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_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 wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||||
@@ -122,6 +124,7 @@ pub struct Globals {
|
|||||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||||
|
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
|
||||||
pub executor: ForegroundExecutor,
|
pub executor: ForegroundExecutor,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ impl Globals {
|
|||||||
qh: QueueHandle<WaylandClientStatePtr>,
|
qh: QueueHandle<WaylandClientStatePtr>,
|
||||||
seat: wl_seat::WlSeat,
|
seat: wl_seat::WlSeat,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let dialog_v = XdgWmDialogV1::interface().version;
|
||||||
Globals {
|
Globals {
|
||||||
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
activation: globals.bind(&qh, 1..=1, ()).ok(),
|
||||||
compositor: globals
|
compositor: globals
|
||||||
@@ -160,6 +164,7 @@ impl Globals {
|
|||||||
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
|
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
|
||||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||||
text_input_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,
|
executor,
|
||||||
qh,
|
qh,
|
||||||
}
|
}
|
||||||
@@ -729,10 +734,7 @@ impl LinuxClient for WaylandClient {
|
|||||||
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
) -> anyhow::Result<Box<dyn PlatformWindow>> {
|
||||||
let mut state = self.0.borrow_mut();
|
let mut state = self.0.borrow_mut();
|
||||||
|
|
||||||
let parent = state
|
let parent = state.keyboard_focused_window.clone();
|
||||||
.keyboard_focused_window
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|w| w.toplevel());
|
|
||||||
|
|
||||||
let (window, surface_id) = WaylandWindow::new(
|
let (window, surface_id) = WaylandWindow::new(
|
||||||
handle,
|
handle,
|
||||||
@@ -751,7 +753,12 @@ impl LinuxClient for WaylandClient {
|
|||||||
fn set_cursor_style(&self, style: CursorStyle) {
|
fn set_cursor_style(&self, style: CursorStyle) {
|
||||||
let mut state = self.0.borrow_mut();
|
let mut state = self.0.borrow_mut();
|
||||||
|
|
||||||
let need_update = state.cursor_style != Some(style);
|
let need_update = state.cursor_style != Some(style)
|
||||||
|
&& (state.mouse_focused_window.is_none()
|
||||||
|
|| state
|
||||||
|
.mouse_focused_window
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|w| !w.is_blocked()));
|
||||||
|
|
||||||
if need_update {
|
if need_update {
|
||||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||||
@@ -1011,7 +1018,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_window(
|
pub(crate) fn get_window(
|
||||||
mut state: &mut RefMut<WaylandClientState>,
|
mut state: &mut RefMut<WaylandClientState>,
|
||||||
surface_id: &ObjectId,
|
surface_id: &ObjectId,
|
||||||
) -> Option<WaylandWindowStatePtr> {
|
) -> Option<WaylandWindowStatePtr> {
|
||||||
@@ -1654,6 +1661,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
|||||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
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 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
|
if state
|
||||||
.keyboard_focused_window
|
.keyboard_focused_window
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2225,3 +2256,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &XdgWmDialogV1,
|
||||||
|
_: <XdgWmDialogV1 as Proxy>::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
|
||||||
|
fn event(
|
||||||
|
_state: &mut Self,
|
||||||
|
_proxy: &XdgDialogV1,
|
||||||
|
_event: <XdgDialogV1 as Proxy>::Event,
|
||||||
|
_data: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
_qhandle: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use blade_graphics as gpu;
|
use blade_graphics as gpu;
|
||||||
use collections::HashMap;
|
use collections::{FxHashSet, HashMap};
|
||||||
use futures::channel::oneshot::Receiver;
|
use futures::channel::oneshot::Receiver;
|
||||||
|
|
||||||
use raw_window_handle as rwh;
|
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::xdg::shell::client::xdg_toplevel::{self};
|
||||||
use wayland_protocols::{
|
use wayland_protocols::{
|
||||||
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
|
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
|
||||||
xdg::shell::client::xdg_toplevel::XdgToplevel,
|
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
|
||||||
};
|
};
|
||||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
|
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,
|
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
|
||||||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||||
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||||
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
|
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
|
||||||
layer_shell::LayerShellNotSupportedError, px, size,
|
layer_shell::LayerShellNotSupportedError, px, size,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -87,6 +87,8 @@ struct InProgressConfigure {
|
|||||||
pub struct WaylandWindowState {
|
pub struct WaylandWindowState {
|
||||||
surface_state: WaylandSurfaceState,
|
surface_state: WaylandSurfaceState,
|
||||||
acknowledged_first_configure: bool,
|
acknowledged_first_configure: bool,
|
||||||
|
parent: Option<WaylandWindowStatePtr>,
|
||||||
|
children: FxHashSet<ObjectId>,
|
||||||
pub surface: wl_surface::WlSurface,
|
pub surface: wl_surface::WlSurface,
|
||||||
app_id: Option<String>,
|
app_id: Option<String>,
|
||||||
appearance: WindowAppearance,
|
appearance: WindowAppearance,
|
||||||
@@ -126,7 +128,7 @@ impl WaylandSurfaceState {
|
|||||||
surface: &wl_surface::WlSurface,
|
surface: &wl_surface::WlSurface,
|
||||||
globals: &Globals,
|
globals: &Globals,
|
||||||
params: &WindowParams,
|
params: &WindowParams,
|
||||||
parent: Option<XdgToplevel>,
|
parent: Option<WaylandWindowStatePtr>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
// For layer_shell windows, create a layer surface instead of an xdg surface
|
// For layer_shell windows, create a layer surface instead of an xdg surface
|
||||||
if let WindowKind::LayerShell(options) = ¶ms.kind {
|
if let WindowKind::LayerShell(options) = ¶ms.kind {
|
||||||
@@ -178,10 +180,28 @@ impl WaylandSurfaceState {
|
|||||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||||
|
|
||||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||||
if params.kind == WindowKind::Floating {
|
let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
|
||||||
toplevel.set_parent(parent.as_ref());
|
|
||||||
|
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||||
|
toplevel.set_parent(xdg_parent.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dialog = if params.kind == WindowKind::Dialog {
|
||||||
|
let dialog = globals.dialog.as_ref().map(|dialog| {
|
||||||
|
let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
|
||||||
|
xdg_dialog.set_modal();
|
||||||
|
xdg_dialog
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(parent) = parent.as_ref() {
|
||||||
|
parent.add_child(surface.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(size) = params.window_min_size {
|
if let Some(size) = params.window_min_size {
|
||||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||||
}
|
}
|
||||||
@@ -198,6 +218,7 @@ impl WaylandSurfaceState {
|
|||||||
xdg_surface,
|
xdg_surface,
|
||||||
toplevel,
|
toplevel,
|
||||||
decoration,
|
decoration,
|
||||||
|
dialog,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,6 +227,7 @@ pub struct WaylandXdgSurfaceState {
|
|||||||
xdg_surface: xdg_surface::XdgSurface,
|
xdg_surface: xdg_surface::XdgSurface,
|
||||||
toplevel: xdg_toplevel::XdgToplevel,
|
toplevel: xdg_toplevel::XdgToplevel,
|
||||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||||
|
dialog: Option<XdgDialogV1>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WaylandLayerSurfaceState {
|
pub struct WaylandLayerSurfaceState {
|
||||||
@@ -258,7 +280,13 @@ impl WaylandSurfaceState {
|
|||||||
xdg_surface,
|
xdg_surface,
|
||||||
toplevel,
|
toplevel,
|
||||||
decoration: _decoration,
|
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.
|
// The role object (toplevel) must always be destroyed before the xdg_surface.
|
||||||
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
|
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
|
||||||
toplevel.destroy();
|
toplevel.destroy();
|
||||||
@@ -288,6 +316,7 @@ impl WaylandWindowState {
|
|||||||
globals: Globals,
|
globals: Globals,
|
||||||
gpu_context: &BladeContext,
|
gpu_context: &BladeContext,
|
||||||
options: WindowParams,
|
options: WindowParams,
|
||||||
|
parent: Option<WaylandWindowStatePtr>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let renderer = {
|
let renderer = {
|
||||||
let raw_window = RawWindow {
|
let raw_window = RawWindow {
|
||||||
@@ -319,6 +348,8 @@ impl WaylandWindowState {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
surface_state,
|
surface_state,
|
||||||
acknowledged_first_configure: false,
|
acknowledged_first_configure: false,
|
||||||
|
parent,
|
||||||
|
children: FxHashSet::default(),
|
||||||
surface,
|
surface,
|
||||||
app_id: None,
|
app_id: None,
|
||||||
blur: None,
|
blur: None,
|
||||||
@@ -391,6 +422,10 @@ impl Drop for WaylandWindow {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let mut state = self.0.state.borrow_mut();
|
let mut state = self.0.state.borrow_mut();
|
||||||
let surface_id = state.surface.id();
|
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();
|
let client = state.client.clone();
|
||||||
|
|
||||||
state.renderer.destroy();
|
state.renderer.destroy();
|
||||||
@@ -448,10 +483,10 @@ impl WaylandWindow {
|
|||||||
client: WaylandClientStatePtr,
|
client: WaylandClientStatePtr,
|
||||||
params: WindowParams,
|
params: WindowParams,
|
||||||
appearance: WindowAppearance,
|
appearance: WindowAppearance,
|
||||||
parent: Option<XdgToplevel>,
|
parent: Option<WaylandWindowStatePtr>,
|
||||||
) -> anyhow::Result<(Self, ObjectId)> {
|
) -> anyhow::Result<(Self, ObjectId)> {
|
||||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||||
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent)?;
|
let surface_state = WaylandSurfaceState::new(&surface, &globals, ¶ms, parent.clone())?;
|
||||||
|
|
||||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||||
@@ -473,6 +508,7 @@ impl WaylandWindow {
|
|||||||
globals,
|
globals,
|
||||||
gpu_context,
|
gpu_context,
|
||||||
params,
|
params,
|
||||||
|
parent,
|
||||||
)?)),
|
)?)),
|
||||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||||
});
|
});
|
||||||
@@ -501,6 +537,16 @@ impl WaylandWindowStatePtr {
|
|||||||
Rc::ptr_eq(&self.state, &other.state)
|
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) {
|
pub fn frame(&self) {
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||||
@@ -818,6 +864,9 @@ impl WaylandWindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ime(&self, ime: ImeInput) {
|
pub fn handle_ime(&self, ime: ImeInput) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if let Some(mut input_handler) = state.input_handler.take() {
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
drop(state);
|
drop(state);
|
||||||
@@ -894,6 +943,21 @@ impl WaylandWindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&self) {
|
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();
|
let mut callbacks = self.callbacks.borrow_mut();
|
||||||
if let Some(fun) = callbacks.close.take() {
|
if let Some(fun) = callbacks.close.take() {
|
||||||
fun()
|
fun()
|
||||||
@@ -901,6 +965,9 @@ impl WaylandWindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(&self, input: PlatformInput) {
|
pub fn handle_input(&self, input: PlatformInput) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||||
&& !fun(input.clone()).propagate
|
&& !fun(input.clone()).propagate
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ pub struct X11ClientState {
|
|||||||
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
|
||||||
|
|
||||||
impl X11ClientStatePtr {
|
impl X11ClientStatePtr {
|
||||||
fn get_client(&self) -> Option<X11Client> {
|
pub fn get_client(&self) -> Option<X11Client> {
|
||||||
self.0.upgrade().map(X11Client)
|
self.0.upgrade().map(X11Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,7 +752,7 @@ impl X11Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
|
||||||
let state = self.0.borrow();
|
let state = self.0.borrow();
|
||||||
state
|
state
|
||||||
.windows
|
.windows
|
||||||
@@ -789,12 +789,12 @@ impl X11Client {
|
|||||||
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
|
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
|
||||||
let mut state = self.0.borrow_mut();
|
let mut state = self.0.borrow_mut();
|
||||||
|
|
||||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
|
||||||
// window "x" button clicked by user
|
// window "x" button clicked by user
|
||||||
if window.should_close() {
|
// Rest of the close logic is handled in drop_window()
|
||||||
// Rest of the close logic is handled in drop_window()
|
drop(state);
|
||||||
window.close();
|
window.close();
|
||||||
}
|
state = self.0.borrow_mut();
|
||||||
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
||||||
window.state.borrow_mut().last_sync_counter =
|
window.state.borrow_mut().last_sync_counter =
|
||||||
Some(x11rb::protocol::sync::Int64 {
|
Some(x11rb::protocol::sync::Int64 {
|
||||||
@@ -1216,6 +1216,33 @@ impl X11Client {
|
|||||||
Event::XinputMotion(event) => {
|
Event::XinputMotion(event) => {
|
||||||
let window = self.get_window(event.event)?;
|
let window = self.get_window(event.event)?;
|
||||||
let mut state = self.0.borrow_mut();
|
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 pressed_button = pressed_button_from_mask(event.button_mask[0]);
|
||||||
let position = point(
|
let position = point(
|
||||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||||
@@ -1489,7 +1516,7 @@ impl LinuxClient for X11Client {
|
|||||||
let parent_window = state
|
let parent_window = state
|
||||||
.keyboard_focused_window
|
.keyboard_focused_window
|
||||||
.and_then(|focused_window| state.windows.get(&focused_window))
|
.and_then(|focused_window| state.windows.get(&focused_window))
|
||||||
.map(|window| window.window.x_window);
|
.map(|w| w.window.clone());
|
||||||
let x_window = state
|
let x_window = state
|
||||||
.xcb_connection
|
.xcb_connection
|
||||||
.generate_id()
|
.generate_id()
|
||||||
@@ -1544,7 +1571,15 @@ impl LinuxClient for X11Client {
|
|||||||
.cursor_styles
|
.cursor_styles
|
||||||
.get(&focused_window)
|
.get(&focused_window)
|
||||||
.unwrap_or(&CursorStyle::Arrow);
|
.unwrap_or(&CursorStyle::Arrow);
|
||||||
if *current_style == style {
|
|
||||||
|
let window = state
|
||||||
|
.mouse_focused_window
|
||||||
|
.and_then(|w| state.windows.get(&w));
|
||||||
|
|
||||||
|
let should_change = *current_style != style
|
||||||
|
&& (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
|
||||||
|
|
||||||
|
if !should_change {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use blade_graphics as gpu;
|
use blade_graphics as gpu;
|
||||||
|
use collections::FxHashSet;
|
||||||
use raw_window_handle as rwh;
|
use raw_window_handle as rwh;
|
||||||
use util::{ResultExt, maybe};
|
use util::{ResultExt, maybe};
|
||||||
use x11rb::{
|
use x11rb::{
|
||||||
@@ -74,6 +75,7 @@ x11rb::atom_manager! {
|
|||||||
_NET_WM_WINDOW_TYPE,
|
_NET_WM_WINDOW_TYPE,
|
||||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||||
_NET_WM_WINDOW_TYPE_DIALOG,
|
_NET_WM_WINDOW_TYPE_DIALOG,
|
||||||
|
_NET_WM_STATE_MODAL,
|
||||||
_NET_WM_SYNC,
|
_NET_WM_SYNC,
|
||||||
_NET_SUPPORTED,
|
_NET_SUPPORTED,
|
||||||
_MOTIF_WM_HINTS,
|
_MOTIF_WM_HINTS,
|
||||||
@@ -249,6 +251,8 @@ pub struct Callbacks {
|
|||||||
|
|
||||||
pub struct X11WindowState {
|
pub struct X11WindowState {
|
||||||
pub destroyed: bool,
|
pub destroyed: bool,
|
||||||
|
parent: Option<X11WindowStatePtr>,
|
||||||
|
children: FxHashSet<xproto::Window>,
|
||||||
client: X11ClientStatePtr,
|
client: X11ClientStatePtr,
|
||||||
executor: ForegroundExecutor,
|
executor: ForegroundExecutor,
|
||||||
atoms: XcbAtoms,
|
atoms: XcbAtoms,
|
||||||
@@ -394,7 +398,7 @@ impl X11WindowState {
|
|||||||
atoms: &XcbAtoms,
|
atoms: &XcbAtoms,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
appearance: WindowAppearance,
|
appearance: WindowAppearance,
|
||||||
parent_window: Option<xproto::Window>,
|
parent_window: Option<X11WindowStatePtr>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let x_screen_index = params
|
let x_screen_index = params
|
||||||
.display_id
|
.display_id
|
||||||
@@ -546,8 +550,8 @@ impl X11WindowState {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.kind == WindowKind::Floating {
|
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
|
||||||
if let Some(parent_window) = parent_window {
|
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
|
||||||
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
|
// 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
|
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
|
||||||
// place the floating window in relation to the main window.
|
// place the floating window in relation to the main window.
|
||||||
@@ -563,11 +567,23 @@ impl X11WindowState {
|
|||||||
),
|
),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = if params.kind == WindowKind::Dialog
|
||||||
|
&& let Some(parent) = parent_window
|
||||||
|
{
|
||||||
|
parent.add_child(x_window);
|
||||||
|
|
||||||
|
Some(parent)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if params.kind == WindowKind::Dialog {
|
||||||
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
|
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
|
||||||
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
|
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
|
||||||
check_reply(
|
check_reply(
|
||||||
|| "X11 ChangeProperty32 setting window type for floating window failed.",
|
|| "X11 ChangeProperty32 setting window type for dialog window failed.",
|
||||||
xcb.change_property32(
|
xcb.change_property32(
|
||||||
xproto::PropMode::REPLACE,
|
xproto::PropMode::REPLACE,
|
||||||
x_window,
|
x_window,
|
||||||
@@ -576,6 +592,20 @@ impl X11WindowState {
|
|||||||
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
|
&[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(
|
check_reply(
|
||||||
@@ -667,6 +697,8 @@ impl X11WindowState {
|
|||||||
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
parent,
|
||||||
|
children: FxHashSet::default(),
|
||||||
client,
|
client,
|
||||||
executor,
|
executor,
|
||||||
display,
|
display,
|
||||||
@@ -720,6 +752,11 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
|
|||||||
impl Drop for X11Window {
|
impl Drop for X11Window {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let mut state = self.0.state.borrow_mut();
|
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();
|
state.renderer.destroy();
|
||||||
|
|
||||||
let destroy_x_window = maybe!({
|
let destroy_x_window = maybe!({
|
||||||
@@ -734,8 +771,6 @@ impl Drop for X11Window {
|
|||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
if destroy_x_window.is_some() {
|
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;
|
state.destroyed = true;
|
||||||
|
|
||||||
let this_ptr = self.0.clone();
|
let this_ptr = self.0.clone();
|
||||||
@@ -773,7 +808,7 @@ impl X11Window {
|
|||||||
atoms: &XcbAtoms,
|
atoms: &XcbAtoms,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
appearance: WindowAppearance,
|
appearance: WindowAppearance,
|
||||||
parent_window: Option<xproto::Window>,
|
parent_window: Option<X11WindowStatePtr>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
let ptr = X11WindowStatePtr {
|
let ptr = X11WindowStatePtr {
|
||||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||||
@@ -979,7 +1014,31 @@ impl X11WindowStatePtr {
|
|||||||
Ok(())
|
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) {
|
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();
|
let mut callbacks = self.callbacks.borrow_mut();
|
||||||
if let Some(fun) = callbacks.close.take() {
|
if let Some(fun) = callbacks.close.take() {
|
||||||
fun()
|
fun()
|
||||||
@@ -994,6 +1053,9 @@ impl X11WindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(&self, input: PlatformInput) {
|
pub fn handle_input(&self, input: PlatformInput) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
|
||||||
&& !fun(input.clone()).propagate
|
&& !fun(input.clone()).propagate
|
||||||
{
|
{
|
||||||
@@ -1016,6 +1078,9 @@ impl X11WindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ime_commit(&self, text: String) {
|
pub fn handle_ime_commit(&self, text: String) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if let Some(mut input_handler) = state.input_handler.take() {
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
drop(state);
|
drop(state);
|
||||||
@@ -1026,6 +1091,9 @@ impl X11WindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ime_preedit(&self, text: String) {
|
pub fn handle_ime_preedit(&self, text: String) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if let Some(mut input_handler) = state.input_handler.take() {
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
drop(state);
|
drop(state);
|
||||||
@@ -1036,6 +1104,9 @@ impl X11WindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ime_unmark(&self) {
|
pub fn handle_ime_unmark(&self) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if let Some(mut input_handler) = state.input_handler.take() {
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
drop(state);
|
drop(state);
|
||||||
@@ -1046,6 +1117,9 @@ impl X11WindowStatePtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_ime_delete(&self) {
|
pub fn handle_ime_delete(&self) {
|
||||||
|
if self.is_blocked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if let Some(mut input_handler) = state.input_handler.take() {
|
if let Some(mut input_handler) = state.input_handler.take() {
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|||||||
@@ -62,9 +62,12 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
|
|||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
|
||||||
NSWindowStyleMask::from_bits_retain(1 << 7);
|
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)]
|
#[allow(non_upper_case_globals)]
|
||||||
const NSNormalWindowLevel: NSInteger = 0;
|
const NSNormalWindowLevel: NSInteger = 0;
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
|
const NSFloatingWindowLevel: NSInteger = 3;
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
const NSPopUpWindowLevel: NSInteger = 101;
|
const NSPopUpWindowLevel: NSInteger = 101;
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
|
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
|
||||||
@@ -423,6 +426,8 @@ struct MacWindowState {
|
|||||||
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
|
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
|
||||||
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
|
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
|
||||||
activated_least_once: bool,
|
activated_least_once: bool,
|
||||||
|
// The parent window if this window is a sheet (Dialog kind)
|
||||||
|
sheet_parent: Option<id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacWindowState {
|
impl MacWindowState {
|
||||||
@@ -622,11 +627,16 @@ impl MacWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let native_window: id = match kind {
|
let native_window: id = match kind {
|
||||||
WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
|
WindowKind::Normal => {
|
||||||
|
msg_send![WINDOW_CLASS, alloc]
|
||||||
|
}
|
||||||
WindowKind::PopUp => {
|
WindowKind::PopUp => {
|
||||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||||
msg_send![PANEL_CLASS, alloc]
|
msg_send![PANEL_CLASS, alloc]
|
||||||
}
|
}
|
||||||
|
WindowKind::Floating | WindowKind::Dialog => {
|
||||||
|
msg_send![PANEL_CLASS, alloc]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let display = display_id
|
let display = display_id
|
||||||
@@ -729,6 +739,7 @@ impl MacWindow {
|
|||||||
select_previous_tab_callback: None,
|
select_previous_tab_callback: None,
|
||||||
toggle_tab_bar_callback: None,
|
toggle_tab_bar_callback: None,
|
||||||
activated_least_once: false,
|
activated_least_once: false,
|
||||||
|
sheet_parent: None,
|
||||||
})));
|
})));
|
||||||
|
|
||||||
(*native_window).set_ivar(
|
(*native_window).set_ivar(
|
||||||
@@ -779,9 +790,18 @@ impl MacWindow {
|
|||||||
content_view.addSubview_(native_view.autorelease());
|
content_view.addSubview_(native_view.autorelease());
|
||||||
native_window.makeFirstResponder_(native_view);
|
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 {
|
match kind {
|
||||||
WindowKind::Normal | WindowKind::Floating => {
|
WindowKind::Normal | WindowKind::Floating => {
|
||||||
native_window.setLevel_(NSNormalWindowLevel);
|
if kind == WindowKind::Floating {
|
||||||
|
// Let the window float keep above normal windows.
|
||||||
|
native_window.setLevel_(NSFloatingWindowLevel);
|
||||||
|
} else {
|
||||||
|
native_window.setLevel_(NSNormalWindowLevel);
|
||||||
|
}
|
||||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||||
|
|
||||||
if let Some(tabbing_identifier) = tabbing_identifier {
|
if let Some(tabbing_identifier) = tabbing_identifier {
|
||||||
@@ -816,10 +836,23 @@ impl MacWindow {
|
|||||||
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
|
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
|
if allows_automatic_window_tabbing
|
||||||
&& !main_window.is_null()
|
&& !main_window.is_null()
|
||||||
&& main_window != native_window
|
&& main_window != native_window
|
||||||
@@ -861,7 +894,11 @@ impl MacWindow {
|
|||||||
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
|
// 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.
|
// is different from the primary screen.
|
||||||
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
|
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
|
||||||
window.0.lock().move_traffic_light();
|
{
|
||||||
|
let mut window_state = window.0.lock();
|
||||||
|
window_state.move_traffic_light();
|
||||||
|
window_state.sheet_parent = sheet_parent;
|
||||||
|
}
|
||||||
|
|
||||||
pool.drain();
|
pool.drain();
|
||||||
|
|
||||||
@@ -938,6 +975,7 @@ impl Drop for MacWindow {
|
|||||||
let mut this = self.0.lock();
|
let mut this = self.0.lock();
|
||||||
this.renderer.destroy();
|
this.renderer.destroy();
|
||||||
let window = this.native_window;
|
let window = this.native_window;
|
||||||
|
let sheet_parent = this.sheet_parent.take();
|
||||||
this.display_link.take();
|
this.display_link.take();
|
||||||
unsafe {
|
unsafe {
|
||||||
this.native_window.setDelegate_(nil);
|
this.native_window.setDelegate_(nil);
|
||||||
@@ -946,6 +984,9 @@ impl Drop for MacWindow {
|
|||||||
this.executor
|
this.executor
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
if let Some(parent) = sheet_parent {
|
||||||
|
let _: () = msg_send![parent, endSheet: window];
|
||||||
|
}
|
||||||
window.close();
|
window.close();
|
||||||
window.autorelease();
|
window.autorelease();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
TestDisplay, TestPlatformWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
@@ -26,7 +26,7 @@ pub(crate) struct TestPlatform {
|
|||||||
background_executor: BackgroundExecutor,
|
background_executor: BackgroundExecutor,
|
||||||
foreground_executor: ForegroundExecutor,
|
foreground_executor: ForegroundExecutor,
|
||||||
|
|
||||||
pub(crate) active_window: RefCell<Option<TestWindow>>,
|
pub(crate) active_window: RefCell<Option<TestPlatformWindow>>,
|
||||||
active_display: Rc<dyn PlatformDisplay>,
|
active_display: Rc<dyn PlatformDisplay>,
|
||||||
active_cursor: Mutex<CursorStyle>,
|
active_cursor: Mutex<CursorStyle>,
|
||||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||||
@@ -196,7 +196,7 @@ impl TestPlatform {
|
|||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
|
pub(crate) fn set_active_window(&self, window: Option<TestPlatformWindow>) {
|
||||||
let executor = self.foreground_executor();
|
let executor = self.foreground_executor();
|
||||||
let previous_window = self.active_window.borrow_mut().take();
|
let previous_window = self.active_window.borrow_mut().take();
|
||||||
self.active_window.borrow_mut().clone_from(&window);
|
self.active_window.borrow_mut().clone_from(&window);
|
||||||
@@ -314,7 +314,7 @@ impl Platform for TestPlatform {
|
|||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
params: WindowParams,
|
params: WindowParams,
|
||||||
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
|
||||||
let window = TestWindow::new(
|
let window = TestPlatformWindow::new(
|
||||||
handle,
|
handle,
|
||||||
params,
|
params,
|
||||||
self.weak.clone(),
|
self.weak.clone(),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::{
|
|||||||
sync::{self, Arc},
|
sync::{self, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct TestWindowState {
|
pub(crate) struct TestPlatformWindowState {
|
||||||
pub(crate) bounds: Bounds<Pixels>,
|
pub(crate) bounds: Bounds<Pixels>,
|
||||||
pub(crate) handle: AnyWindowHandle,
|
pub(crate) handle: AnyWindowHandle,
|
||||||
display: Rc<dyn PlatformDisplay>,
|
display: Rc<dyn PlatformDisplay>,
|
||||||
@@ -32,9 +32,9 @@ pub(crate) struct TestWindowState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
|
pub(crate) struct TestPlatformWindow(pub(crate) Rc<Mutex<TestPlatformWindowState>>);
|
||||||
|
|
||||||
impl HasWindowHandle for TestWindow {
|
impl HasWindowHandle for TestPlatformWindow {
|
||||||
fn window_handle(
|
fn window_handle(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||||
@@ -42,7 +42,7 @@ impl HasWindowHandle for TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasDisplayHandle for TestWindow {
|
impl HasDisplayHandle for TestPlatformWindow {
|
||||||
fn display_handle(
|
fn display_handle(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||||
@@ -50,14 +50,14 @@ impl HasDisplayHandle for TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestWindow {
|
impl TestPlatformWindow {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
params: WindowParams,
|
params: WindowParams,
|
||||||
platform: Weak<TestPlatform>,
|
platform: Weak<TestPlatform>,
|
||||||
display: Rc<dyn PlatformDisplay>,
|
display: Rc<dyn PlatformDisplay>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self(Rc::new(Mutex::new(TestWindowState {
|
Self(Rc::new(Mutex::new(TestPlatformWindowState {
|
||||||
bounds: params.bounds,
|
bounds: params.bounds,
|
||||||
display,
|
display,
|
||||||
platform,
|
platform,
|
||||||
@@ -111,7 +111,7 @@ impl TestWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlatformWindow for TestWindow {
|
impl PlatformWindow for TestPlatformWindow {
|
||||||
fn bounds(&self) -> Bounds<Pixels> {
|
fn bounds(&self) -> Bounds<Pixels> {
|
||||||
self.0.lock().bounds
|
self.0.lock().bounds
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ impl PlatformWindow for TestWindow {
|
|||||||
self.0.lock().sprite_atlas.clone()
|
self.0.lock().sprite_atlas.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ impl WindowsWindowInner {
|
|||||||
lparam: LPARAM,
|
lparam: LPARAM,
|
||||||
) -> LRESULT {
|
) -> LRESULT {
|
||||||
let handled = match msg {
|
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_ACTIVATE => self.handle_activate_msg(wparam),
|
||||||
WM_CREATE => self.handle_create_msg(handle),
|
WM_CREATE => self.handle_create_msg(handle),
|
||||||
WM_MOVE => self.handle_move_msg(handle, lparam),
|
WM_MOVE => self.handle_move_msg(handle, lparam),
|
||||||
@@ -265,6 +270,14 @@ impl WindowsWindowInner {
|
|||||||
|
|
||||||
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
|
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
|
||||||
let callback = { self.state.callbacks.close.take() };
|
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 {
|
if let Some(callback) = callback {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
|
|||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
// ERROR_NOT_FOUND means the credential doesn't exist.
|
// ERROR_NOT_FOUND means the credential doesn't exist.
|
||||||
// Return Ok(None) to match macOS and Linux behavior.
|
// Return Ok(None) to match macOS and Linux behavior.
|
||||||
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
|
if err.code() == ERROR_NOT_FOUND.to_hresult() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ pub(crate) struct WindowsWindowInner {
|
|||||||
pub(crate) validation_number: usize,
|
pub(crate) validation_number: usize,
|
||||||
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
|
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
|
||||||
pub(crate) platform_window_handle: HWND,
|
pub(crate) platform_window_handle: HWND,
|
||||||
|
pub(crate) parent_hwnd: Option<HWND>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowsWindowState {
|
impl WindowsWindowState {
|
||||||
@@ -241,6 +242,7 @@ impl WindowsWindowInner {
|
|||||||
main_receiver: context.main_receiver.clone(),
|
main_receiver: context.main_receiver.clone(),
|
||||||
platform_window_handle: context.platform_window_handle,
|
platform_window_handle: context.platform_window_handle,
|
||||||
system_settings: WindowsSystemSettings::new(context.display),
|
system_settings: WindowsSystemSettings::new(context.display),
|
||||||
|
parent_hwnd: context.parent_hwnd,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +370,7 @@ struct WindowCreateContext {
|
|||||||
disable_direct_composition: bool,
|
disable_direct_composition: bool,
|
||||||
directx_devices: DirectXDevices,
|
directx_devices: DirectXDevices,
|
||||||
invalidate_devices: Arc<AtomicBool>,
|
invalidate_devices: Arc<AtomicBool>,
|
||||||
|
parent_hwnd: Option<HWND>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowsWindow {
|
impl WindowsWindow {
|
||||||
@@ -390,6 +393,20 @@ impl WindowsWindow {
|
|||||||
invalidate_devices,
|
invalidate_devices,
|
||||||
} = creation_info;
|
} = creation_info;
|
||||||
register_window_class(icon);
|
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
|
let hide_title_bar = params
|
||||||
.titlebar
|
.titlebar
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -416,8 +433,14 @@ impl WindowsWindow {
|
|||||||
if params.is_minimizable {
|
if params.is_minimizable {
|
||||||
dwstyle |= WS_MINIMIZEBOX;
|
dwstyle |= WS_MINIMIZEBOX;
|
||||||
}
|
}
|
||||||
|
let dwexstyle = if params.kind == WindowKind::Dialog {
|
||||||
|
dwstyle |= WS_POPUP | WS_CAPTION;
|
||||||
|
WS_EX_DLGMODALFRAME
|
||||||
|
} else {
|
||||||
|
WS_EX_APPWINDOW
|
||||||
|
};
|
||||||
|
|
||||||
(WS_EX_APPWINDOW, dwstyle)
|
(dwexstyle, dwstyle)
|
||||||
};
|
};
|
||||||
if !disable_direct_composition {
|
if !disable_direct_composition {
|
||||||
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
||||||
@@ -449,6 +472,7 @@ impl WindowsWindow {
|
|||||||
disable_direct_composition,
|
disable_direct_composition,
|
||||||
directx_devices,
|
directx_devices,
|
||||||
invalidate_devices,
|
invalidate_devices,
|
||||||
|
parent_hwnd,
|
||||||
};
|
};
|
||||||
let creation_result = unsafe {
|
let creation_result = unsafe {
|
||||||
CreateWindowExW(
|
CreateWindowExW(
|
||||||
@@ -460,7 +484,7 @@ impl WindowsWindow {
|
|||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
CW_USEDEFAULT,
|
CW_USEDEFAULT,
|
||||||
None,
|
parent_hwnd,
|
||||||
None,
|
None,
|
||||||
Some(hinstance.into()),
|
Some(hinstance.into()),
|
||||||
Some(&context as *const _ as *const _),
|
Some(&context as *const _ as *const _),
|
||||||
|
|||||||
@@ -20,6 +20,126 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
|||||||
|
|
||||||
pub(crate) type DrawOrder = u32;
|
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)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Scene {
|
pub(crate) struct Scene {
|
||||||
pub(crate) paint_operations: Vec<PaintOperation>,
|
pub(crate) paint_operations: Vec<PaintOperation>,
|
||||||
@@ -32,6 +152,8 @@ pub(crate) struct Scene {
|
|||||||
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
||||||
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
||||||
pub(crate) surfaces: Vec<PaintSurface>,
|
pub(crate) surfaces: Vec<PaintSurface>,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub(crate) diagnostic_quads: Vec<test_scene::DiagnosticQuad>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
@@ -46,6 +168,8 @@ impl Scene {
|
|||||||
self.monochrome_sprites.clear();
|
self.monochrome_sprites.clear();
|
||||||
self.polychrome_sprites.clear();
|
self.polychrome_sprites.clear();
|
||||||
self.surfaces.clear();
|
self.surfaces.clear();
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
self.diagnostic_quads.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
@@ -124,6 +248,41 @@ 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) {
|
pub fn finish(&mut self) {
|
||||||
self.shadows.sort_by_key(|shadow| shadow.order);
|
self.shadows.sort_by_key(|shadow| shadow.order);
|
||||||
self.quads.sort_by_key(|quad| quad.order);
|
self.quads.sort_by_key(|quad| quad.order);
|
||||||
@@ -134,6 +293,10 @@ impl Scene {
|
|||||||
self.polychrome_sprites
|
self.polychrome_sprites
|
||||||
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
|
||||||
self.surfaces.sort_by_key(|surface| surface.order);
|
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(
|
#[cfg_attr(
|
||||||
@@ -620,7 +783,7 @@ impl Default for TransformationMatrix {
|
|||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub(crate) struct MonochromeSprite {
|
pub(crate) struct MonochromeSprite {
|
||||||
pub order: DrawOrder,
|
pub order: DrawOrder,
|
||||||
pub pad: u32, // align to 8 bytes
|
pub pad: u32,
|
||||||
pub bounds: Bounds<ScaledPixels>,
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
pub content_mask: ContentMask<ScaledPixels>,
|
pub content_mask: ContentMask<ScaledPixels>,
|
||||||
pub color: Hsla,
|
pub color: Hsla,
|
||||||
@@ -638,7 +801,7 @@ impl From<MonochromeSprite> for Primitive {
|
|||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub(crate) struct PolychromeSprite {
|
pub(crate) struct PolychromeSprite {
|
||||||
pub order: DrawOrder,
|
pub order: DrawOrder,
|
||||||
pub pad: u32, // align to 8 bytes
|
pub pad: u32,
|
||||||
pub grayscale: bool,
|
pub grayscale: bool,
|
||||||
pub opacity: f32,
|
pub opacity: f32,
|
||||||
pub bounds: Bounds<ScaledPixels>,
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
|
|||||||
@@ -760,6 +760,11 @@ impl Frame {
|
|||||||
self.tab_stops.clear();
|
self.tab_stops.clear();
|
||||||
self.focus = None;
|
self.focus = None;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
{
|
||||||
|
self.debug_bounds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "inspector", debug_assertions))]
|
#[cfg(any(feature = "inspector", debug_assertions))]
|
||||||
{
|
{
|
||||||
self.next_inspector_instance_ids.clear();
|
self.next_inspector_instance_ids.clear();
|
||||||
@@ -2966,6 +2971,41 @@ 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.
|
/// 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.
|
/// This method should only be called as part of the paint phase of element drawing.
|
||||||
@@ -4966,7 +5006,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.
|
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct AnyWindowHandle {
|
pub struct AnyWindowHandle {
|
||||||
pub(crate) id: WindowId,
|
pub(crate) id: WindowId,
|
||||||
state_type: TypeId,
|
state_type: TypeId,
|
||||||
|
|||||||
@@ -1801,9 +1801,7 @@ impl Buffer {
|
|||||||
self.syntax_map.lock().did_parse(syntax_snapshot);
|
self.syntax_map.lock().did_parse(syntax_snapshot);
|
||||||
self.request_autoindent(cx);
|
self.request_autoindent(cx);
|
||||||
self.parse_status.0.send(ParseStatus::Idle).unwrap();
|
self.parse_status.0.send(ParseStatus::Idle).unwrap();
|
||||||
if self.text.version() != *self.tree_sitter_data.version() {
|
self.invalidate_tree_sitter_data(self.text.snapshot());
|
||||||
self.invalidate_tree_sitter_data(self.text.snapshot());
|
|
||||||
}
|
|
||||||
cx.emit(BufferEvent::Reparsed);
|
cx.emit(BufferEvent::Reparsed);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,6 +295,23 @@ impl LspInstaller for TyLspAdapter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_if_user_installed(
|
||||||
|
&self,
|
||||||
|
delegate: &dyn LspAdapterDelegate,
|
||||||
|
_: Option<Toolchain>,
|
||||||
|
_: &AsyncApp,
|
||||||
|
) -> Option<LanguageServerBinary> {
|
||||||
|
let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let env = delegate.shell_env().await;
|
||||||
|
Some(LanguageServerBinary {
|
||||||
|
path: ty_bin,
|
||||||
|
env: Some(env),
|
||||||
|
arguments: vec!["server".into()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_server_binary(
|
async fn fetch_server_binary(
|
||||||
&self,
|
&self,
|
||||||
latest_version: Self::BinaryVersion,
|
latest_version: Self::BinaryVersion,
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
|
|||||||
| lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
|
| lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
|
||||||
) = completion.text_edit.as_ref()
|
) = completion.text_edit.as_ref()
|
||||||
&& let Ok(mut snippet) = snippet::Snippet::parse(new_text)
|
&& let Ok(mut snippet) = snippet::Snippet::parse(new_text)
|
||||||
&& !snippet.tabstops.is_empty()
|
&& snippet.tabstops.len() > 1
|
||||||
{
|
{
|
||||||
label = String::new();
|
label = String::new();
|
||||||
|
|
||||||
@@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter {
|
|||||||
0..label.rfind('(').unwrap_or(completion.label.len()),
|
0..label.rfind('(').unwrap_or(completion.label.len()),
|
||||||
highlight_id,
|
highlight_id,
|
||||||
));
|
));
|
||||||
} else if detail_left.is_none() {
|
} else if detail_left.is_none()
|
||||||
|
&& kind != Some(lsp::CompletionItemKind::SNIPPET)
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1597,6 +1599,40 @@ mod tests {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Postfix completion without actual tabstops (only implicit final $0)
|
||||||
|
// The label should use completion.label so it can be filtered by "ref"
|
||||||
|
let ref_completion = adapter
|
||||||
|
.label_for_completion(
|
||||||
|
&lsp::CompletionItem {
|
||||||
|
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||||
|
label: "ref".to_string(),
|
||||||
|
filter_text: Some("ref".to_string()),
|
||||||
|
label_details: Some(CompletionItemLabelDetails {
|
||||||
|
detail: None,
|
||||||
|
description: Some("&expr".to_string()),
|
||||||
|
}),
|
||||||
|
detail: Some("&expr".to_string()),
|
||||||
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: lsp::Range::default(),
|
||||||
|
new_text: "&String::new()".to_string(),
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&language,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
ref_completion.is_some(),
|
||||||
|
"ref postfix completion should have a label"
|
||||||
|
);
|
||||||
|
let ref_label = ref_completion.unwrap();
|
||||||
|
let filter_text = &ref_label.text[ref_label.filter_range.clone()];
|
||||||
|
assert!(
|
||||||
|
filter_text.contains("ref"),
|
||||||
|
"filter range text '{filter_text}' should contain 'ref' for filtering to work",
|
||||||
|
);
|
||||||
|
|
||||||
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
|
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
|
||||||
let res = adapter
|
let res = adapter
|
||||||
.label_for_completion(
|
.label_for_completion(
|
||||||
|
|||||||
@@ -155,15 +155,15 @@ impl Model {
|
|||||||
pub fn max_token_count(&self) -> u64 {
|
pub fn max_token_count(&self) -> u64 {
|
||||||
match self {
|
match self {
|
||||||
Self::CodestralLatest => 256000,
|
Self::CodestralLatest => 256000,
|
||||||
Self::MistralLargeLatest => 131000,
|
Self::MistralLargeLatest => 256000,
|
||||||
Self::MistralMediumLatest => 128000,
|
Self::MistralMediumLatest => 128000,
|
||||||
Self::MistralSmallLatest => 32000,
|
Self::MistralSmallLatest => 32000,
|
||||||
Self::MagistralMediumLatest => 40000,
|
Self::MagistralMediumLatest => 128000,
|
||||||
Self::MagistralSmallLatest => 40000,
|
Self::MagistralSmallLatest => 128000,
|
||||||
Self::OpenMistralNemo => 131000,
|
Self::OpenMistralNemo => 131000,
|
||||||
Self::OpenCodestralMamba => 256000,
|
Self::OpenCodestralMamba => 256000,
|
||||||
Self::DevstralMediumLatest => 128000,
|
Self::DevstralMediumLatest => 256000,
|
||||||
Self::DevstralSmallLatest => 262144,
|
Self::DevstralSmallLatest => 256000,
|
||||||
Self::Pixtral12BLatest => 128000,
|
Self::Pixtral12BLatest => 128000,
|
||||||
Self::PixtralLargeLatest => 128000,
|
Self::PixtralLargeLatest => 128000,
|
||||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||||
|
|||||||
@@ -2610,9 +2610,8 @@ impl MultiBuffer {
|
|||||||
for range in ranges {
|
for range in ranges {
|
||||||
let range = range.to_point(&snapshot);
|
let range = range.to_point(&snapshot);
|
||||||
let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
|
let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
|
||||||
let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0));
|
let end = (snapshot.point_to_offset(Point::new(range.end.row + 1, 0)) + 1usize)
|
||||||
let start = start.saturating_sub_usize(1);
|
.min(snapshot.len());
|
||||||
let end = snapshot.len().min(end + 1usize);
|
|
||||||
cursor.seek(&start, Bias::Right);
|
cursor.seek(&start, Bias::Right);
|
||||||
while let Some(item) = cursor.item() {
|
while let Some(item) = cursor.item() {
|
||||||
if *cursor.start() >= end {
|
if *cursor.start() >= end {
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ impl AgentServerStore {
|
|||||||
.gemini
|
.gemini
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|settings| settings.ignore_system_version)
|
.and_then(|settings| settings.ignore_system_version)
|
||||||
.unwrap_or(false),
|
.unwrap_or(true),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
self.external_agents.insert(
|
self.external_agents.insert(
|
||||||
|
|||||||
@@ -1672,59 +1672,6 @@ impl GitStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_entries_pending_by_project_paths(
|
|
||||||
&mut self,
|
|
||||||
project_paths: &[ProjectPath],
|
|
||||||
stage: bool,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let buffer_store = &self.buffer_store;
|
|
||||||
|
|
||||||
for project_path in project_paths {
|
|
||||||
let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let buffer_id = buffer.read(cx).remote_id();
|
|
||||||
let Some(diff_state) = self.diffs.get(&buffer_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
diff_state.update(cx, |diff_state, cx| {
|
|
||||||
let Some(uncommitted_diff) = diff_state.uncommitted_diff() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
|
||||||
let file_exists = buffer
|
|
||||||
.read(cx)
|
|
||||||
.file()
|
|
||||||
.is_some_and(|file| file.disk_state().exists());
|
|
||||||
|
|
||||||
let all_hunks: Vec<_> = uncommitted_diff
|
|
||||||
.read(cx)
|
|
||||||
.hunks_intersecting_range(
|
|
||||||
text::Anchor::MIN..text::Anchor::MAX,
|
|
||||||
&buffer_snapshot,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !all_hunks.is_empty() {
|
|
||||||
uncommitted_diff.update(cx, |diff, cx| {
|
|
||||||
diff.stage_or_unstage_hunks(
|
|
||||||
stage,
|
|
||||||
&all_hunks,
|
|
||||||
&buffer_snapshot,
|
|
||||||
file_exists,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn git_clone(
|
pub fn git_clone(
|
||||||
&self,
|
&self,
|
||||||
repo: String,
|
repo: String,
|
||||||
@@ -4253,28 +4200,6 @@ impl Repository {
|
|||||||
save_futures
|
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(
|
pub fn stage_entries(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: Vec<RepoPath>,
|
entries: Vec<RepoPath>,
|
||||||
@@ -4283,9 +4208,6 @@ impl Repository {
|
|||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Task::ready(Ok(()));
|
return Task::ready(Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mark_entries_pending_for_stage(&entries, true, cx);
|
|
||||||
|
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let save_tasks = self.save_buffers(&entries, cx);
|
let save_tasks = self.save_buffers(&entries, cx);
|
||||||
let paths = entries
|
let paths = entries
|
||||||
@@ -4351,9 +4273,6 @@ impl Repository {
|
|||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Task::ready(Ok(()));
|
return Task::ready(Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mark_entries_pending_for_stage(&entries, false, cx);
|
|
||||||
|
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let save_tasks = self.save_buffers(&entries, cx);
|
let save_tasks = self.save_buffers(&entries, cx);
|
||||||
let paths = entries
|
let paths = entries
|
||||||
|
|||||||
@@ -790,8 +790,7 @@ impl TerminalPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
let focus = pane.has_focus(window, cx)
|
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
|
||||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -853,8 +852,7 @@ impl TerminalPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
let focus = pane.has_focus(window, cx)
|
let focus = matches!(reveal_strategy, RevealStrategy::Always);
|
||||||
|| matches!(reveal_strategy, RevealStrategy::Always);
|
|
||||||
pane.add_item(terminal_view, true, focus, None, window, cx);
|
pane.add_item(terminal_view, true, focus, None, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1171,64 +1169,67 @@ pub fn new_terminal_pane(
|
|||||||
let source = tab.pane.clone();
|
let source = tab.pane.clone();
|
||||||
let item_id_to_move = item.item_id();
|
let item_id_to_move = item.item_id();
|
||||||
|
|
||||||
let Ok(new_split_pane) = pane
|
// If no split direction, let the regular pane drop handler take care of it
|
||||||
.drag_split_direction()
|
let Some(split_direction) = pane.drag_split_direction() else {
|
||||||
.map(|split_direction| {
|
return ControlFlow::Continue(());
|
||||||
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(());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match new_split_pane.transpose() {
|
// Gather data synchronously before deferring
|
||||||
// Source pane may be the one currently updated, so defer the move.
|
let is_zoomed = drop_closure_terminal_panel
|
||||||
Ok(Some(new_pane)) => cx
|
.upgrade()
|
||||||
.spawn_in(window, async move |_, cx| {
|
.map(|terminal_panel| {
|
||||||
cx.update(|window, cx| {
|
let terminal_panel = terminal_panel.read(cx);
|
||||||
move_item(
|
if terminal_panel.active_pane == this_pane {
|
||||||
&source,
|
pane.is_zoomed()
|
||||||
&new_pane,
|
} else {
|
||||||
item_id_to_move,
|
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||||
new_pane.read(cx).active_item_index(),
|
}
|
||||||
true,
|
})
|
||||||
window,
|
.unwrap_or(false);
|
||||||
cx,
|
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
let terminal_panel = drop_closure_terminal_panel.clone();
|
||||||
|
|
||||||
|
// Defer the split operation to avoid re-entrancy panic.
|
||||||
|
// The pane may be the one currently being updated, so we cannot
|
||||||
|
// call mark_positions (via split) synchronously.
|
||||||
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
let Ok(new_pane) =
|
||||||
|
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||||
|
let new_pane = new_terminal_pane(
|
||||||
|
workspace, project, is_zoomed, window, cx,
|
||||||
);
|
);
|
||||||
|
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||||
|
terminal_panel.center.split(
|
||||||
|
&this_pane,
|
||||||
|
&new_pane,
|
||||||
|
split_direction,
|
||||||
|
cx,
|
||||||
|
)?;
|
||||||
|
anyhow::Ok(new_pane)
|
||||||
})
|
})
|
||||||
.ok();
|
else {
|
||||||
})
|
return;
|
||||||
.detach(),
|
};
|
||||||
// If we drop into existing pane or current pane,
|
|
||||||
// regular pane drop handler will take care of it,
|
let Some(new_pane) = new_pane.log_err() else {
|
||||||
// using the right tab index for the operation.
|
return;
|
||||||
Ok(None) => return ControlFlow::Continue(()),
|
};
|
||||||
err @ Err(_) => {
|
|
||||||
err.log_err();
|
move_item(
|
||||||
return ControlFlow::Break(());
|
&source,
|
||||||
}
|
&new_pane,
|
||||||
};
|
item_id_to_move,
|
||||||
|
new_pane.read(cx).active_item_index(),
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
} else if let Some(project_path) = item.project_path(cx)
|
} else if let Some(project_path) = item.project_path(cx)
|
||||||
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
|
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
|
|||||||
Severity::Info => (
|
Severity::Info => (
|
||||||
IconName::Info,
|
IconName::Info,
|
||||||
Color::Muted,
|
Color::Muted,
|
||||||
cx.theme().colors().panel_background.opacity(0.),
|
cx.theme().status().info_background.opacity(0.1),
|
||||||
),
|
),
|
||||||
Severity::Success => (
|
Severity::Success => (
|
||||||
IconName::Check,
|
IconName::Check,
|
||||||
|
|||||||
@@ -193,6 +193,12 @@ impl Render for ModalLayer {
|
|||||||
background.fade_out(0.2);
|
background.fade_out(0.2);
|
||||||
this.bg(background)
|
this.bg(background)
|
||||||
})
|
})
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(|this, _, window, cx| {
|
||||||
|
this.hide_modal(window, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.h(px(0.0))
|
.h(px(0.0))
|
||||||
|
|||||||
@@ -3296,4 +3296,53 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(workspace.center_group, new_workspace.center_group);
|
assert_eq!(workspace.center_group, new_workspace.center_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_empty_workspace_window_bounds() {
|
||||||
|
zlog::init_test();
|
||||||
|
|
||||||
|
let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
|
||||||
|
let id = db.next_id().await.unwrap();
|
||||||
|
|
||||||
|
// Create a workspace with empty paths (empty workspace)
|
||||||
|
let empty_paths: &[&str] = &[];
|
||||||
|
let display_uuid = Uuid::new_v4();
|
||||||
|
let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
|
||||||
|
origin: point(px(100.0), px(200.0)),
|
||||||
|
size: size(px(800.0), px(600.0)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let workspace = SerializedWorkspace {
|
||||||
|
id,
|
||||||
|
paths: PathList::new(empty_paths),
|
||||||
|
location: SerializedWorkspaceLocation::Local,
|
||||||
|
center_group: Default::default(),
|
||||||
|
window_bounds: None,
|
||||||
|
display: None,
|
||||||
|
docks: Default::default(),
|
||||||
|
breakpoints: Default::default(),
|
||||||
|
centered_layout: false,
|
||||||
|
session_id: None,
|
||||||
|
window_id: None,
|
||||||
|
user_toolchains: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the workspace (this creates the record with empty paths)
|
||||||
|
db.save_workspace(workspace.clone()).await;
|
||||||
|
|
||||||
|
// Save window bounds separately (as the actual code does via set_window_open_status)
|
||||||
|
db.set_window_open_status(id, window_bounds, display_uuid)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Retrieve it using empty paths
|
||||||
|
let retrieved = db.workspace_for_roots(empty_paths).unwrap();
|
||||||
|
|
||||||
|
// Verify window bounds were persisted
|
||||||
|
assert_eq!(retrieved.id, id);
|
||||||
|
assert!(retrieved.window_bounds.is_some());
|
||||||
|
assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
|
||||||
|
assert!(retrieved.display.is_some());
|
||||||
|
assert_eq!(retrieved.display.unwrap(), display_uuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1748,26 +1748,18 @@ impl Workspace {
|
|||||||
window
|
window
|
||||||
} else {
|
} else {
|
||||||
let window_bounds_override = window_bounds_env_override();
|
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 {
|
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||||
(Some(WindowBounds::Windowed(bounds)), None)
|
(Some(WindowBounds::Windowed(bounds)), None)
|
||||||
} else if let Some(workspace) = serialized_workspace.as_ref() {
|
} else if let Some(workspace) = serialized_workspace.as_ref()
|
||||||
|
&& let Some(display) = workspace.display
|
||||||
|
&& let Some(bounds) = workspace.window_bounds.as_ref()
|
||||||
|
{
|
||||||
// Reopening an existing workspace - restore its saved bounds
|
// Reopening an existing workspace - restore its saved bounds
|
||||||
if let (Some(display), Some(bounds)) =
|
(Some(bounds.0), Some(display))
|
||||||
(workspace.display, workspace.window_bounds.as_ref())
|
} else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
|
||||||
{
|
// New or empty workspace - use the last known window bounds
|
||||||
(Some(bounds.0), Some(display))
|
(Some(bounds), 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 {
|
} else {
|
||||||
// New window - let GPUI's default_bounds() handle cascading
|
// New window - let GPUI's default_bounds() handle cascading
|
||||||
(None, None)
|
(None, None)
|
||||||
@@ -5673,12 +5665,24 @@ impl Workspace {
|
|||||||
persistence::DB.save_workspace(serialized_workspace).await;
|
persistence::DB.save_workspace(serialized_workspace).await;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
|
WorkspaceLocation::DetachFromSession => {
|
||||||
persistence::DB
|
let window_bounds = SerializedWindowBounds(window.window_bounds());
|
||||||
.set_session_id(database_id, None)
|
let display = window.display(cx).and_then(|d| d.uuid().ok());
|
||||||
.await
|
window.spawn(cx, async move |_| {
|
||||||
.log_err();
|
persistence::DB
|
||||||
}),
|
.set_window_open_status(
|
||||||
|
database_id,
|
||||||
|
window_bounds,
|
||||||
|
display.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
persistence::DB
|
||||||
|
.set_session_id(database_id, None)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
WorkspaceLocation::None => Task::ready(()),
|
WorkspaceLocation::None => Task::ready(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ use extension::ExtensionHostProxy;
|
|||||||
use fs::{Fs, RealFs};
|
use fs::{Fs, RealFs};
|
||||||
use futures::{StreamExt, channel::oneshot, future};
|
use futures::{StreamExt, channel::oneshot, future};
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
use git_ui::clone::clone_and_open;
|
|
||||||
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
|
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
|
||||||
|
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
||||||
use project_panel::ProjectPanel;
|
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use remote::RemoteConnectionOptions;
|
use remote::RemoteConnectionOptions;
|
||||||
use reqwest_client::ReqwestClient;
|
use reqwest_client::ReqwestClient;
|
||||||
@@ -35,12 +33,10 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
|||||||
use session::{AppSession, Session};
|
use session::{AppSession, Session};
|
||||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
|
||||||
env,
|
env,
|
||||||
io::{self, IsTerminal},
|
io::{self, IsTerminal},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process,
|
process,
|
||||||
rc::Rc,
|
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
@@ -897,41 +893,6 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
OpenRequestKind::GitClone { repo_url } => {
|
|
||||||
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
|
|
||||||
if window.is_window_active() {
|
|
||||||
clone_and_open(
|
|
||||||
repo_url,
|
|
||||||
cx.weak_entity(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
|
||||||
workspace.focus_panel::<ProjectPanel>(window, cx);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let subscription = Rc::new(RefCell::new(None));
|
|
||||||
subscription.replace(Some(cx.observe_in(&cx.entity(), window, {
|
|
||||||
let subscription = subscription.clone();
|
|
||||||
let repo_url = repo_url.clone();
|
|
||||||
move |_, workspace_entity, window, cx| {
|
|
||||||
if window.is_window_active() && subscription.take().is_some() {
|
|
||||||
clone_and_open(
|
|
||||||
repo_url.clone(),
|
|
||||||
workspace_entity.downgrade(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
|
||||||
workspace.focus_panel::<ProjectPanel>(window, cx);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
OpenRequestKind::GitCommit { sha } => {
|
OpenRequestKind::GitCommit { sha } => {
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let paths_with_position =
|
let paths_with_position =
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use ui::SharedString;
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use util::paths::PathWithPosition;
|
use util::paths::PathWithPosition;
|
||||||
use workspace::PathList;
|
use workspace::PathList;
|
||||||
@@ -59,9 +58,6 @@ pub enum OpenRequestKind {
|
|||||||
/// `None` opens settings without navigating to a specific path.
|
/// `None` opens settings without navigating to a specific path.
|
||||||
setting_path: Option<String>,
|
setting_path: Option<String>,
|
||||||
},
|
},
|
||||||
GitClone {
|
|
||||||
repo_url: SharedString,
|
|
||||||
},
|
|
||||||
GitCommit {
|
GitCommit {
|
||||||
sha: String,
|
sha: String,
|
||||||
},
|
},
|
||||||
@@ -117,8 +113,6 @@ impl OpenRequest {
|
|||||||
this.kind = Some(OpenRequestKind::Setting {
|
this.kind = Some(OpenRequestKind::Setting {
|
||||||
setting_path: Some(setting_path.to_string()),
|
setting_path: Some(setting_path.to_string()),
|
||||||
});
|
});
|
||||||
} else if let Some(clone_path) = url.strip_prefix("zed://git/clone") {
|
|
||||||
this.parse_git_clone_url(clone_path)?
|
|
||||||
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
|
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
|
||||||
this.parse_git_commit_url(commit_path)?
|
this.parse_git_commit_url(commit_path)?
|
||||||
} else if url.starts_with("ssh://") {
|
} else if url.starts_with("ssh://") {
|
||||||
@@ -149,26 +143,6 @@ impl OpenRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> {
|
|
||||||
// Format: /?repo=<url> or ?repo=<url>
|
|
||||||
let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);
|
|
||||||
|
|
||||||
let query = clone_path
|
|
||||||
.strip_prefix('?')
|
|
||||||
.context("invalid git clone url: missing query string")?;
|
|
||||||
|
|
||||||
let repo_url = url::form_urlencoded::parse(query.as_bytes())
|
|
||||||
.find_map(|(key, value)| (key == "repo").then_some(value))
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.context("invalid git clone url: missing repo query parameter")?
|
|
||||||
.to_string()
|
|
||||||
.into();
|
|
||||||
|
|
||||||
self.kind = Some(OpenRequestKind::GitClone { repo_url });
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
|
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
|
||||||
// Format: <sha>?repo=<path>
|
// Format: <sha>?repo=<path>
|
||||||
let (sha, query) = commit_path
|
let (sha, query) = commit_path
|
||||||
@@ -1113,80 +1087,4 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!errored_reuse);
|
assert!(!errored_reuse);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_parse_git_clone_url(cx: &mut TestAppContext) {
|
|
||||||
let _app_state = init_test(cx);
|
|
||||||
|
|
||||||
let request = cx.update(|cx| {
|
|
||||||
OpenRequest::parse(
|
|
||||||
RawOpenRequest {
|
|
||||||
urls: vec![
|
|
||||||
"zed://git/clone/?repo=https://github.com/zed-industries/zed.git".into(),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
match request.kind {
|
|
||||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
|
||||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected GitClone kind"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_parse_git_clone_url_without_slash(cx: &mut TestAppContext) {
|
|
||||||
let _app_state = init_test(cx);
|
|
||||||
|
|
||||||
let request = cx.update(|cx| {
|
|
||||||
OpenRequest::parse(
|
|
||||||
RawOpenRequest {
|
|
||||||
urls: vec![
|
|
||||||
"zed://git/clone?repo=https://github.com/zed-industries/zed.git".into(),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
match request.kind {
|
|
||||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
|
||||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected GitClone kind"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) {
|
|
||||||
let _app_state = init_test(cx);
|
|
||||||
|
|
||||||
let request = cx.update(|cx| {
|
|
||||||
OpenRequest::parse(
|
|
||||||
RawOpenRequest {
|
|
||||||
urls: vec![
|
|
||||||
"zed://git/clone/?repo=https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git"
|
|
||||||
.into(),
|
|
||||||
],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
match request.kind {
|
|
||||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
|
||||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected GitClone kind"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,6 +354,8 @@ pub mod agent {
|
|||||||
ResetAgentZoom,
|
ResetAgentZoom,
|
||||||
/// Toggles the utility/agent pane open/closed state.
|
/// Toggles the utility/agent pane open/closed state.
|
||||||
ToggleAgentPane,
|
ToggleAgentPane,
|
||||||
|
/// Pastes clipboard content without any formatting.
|
||||||
|
PasteRaw,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet
|
|||||||
|
|
||||||
You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
|
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:
|
For more information, see:
|
||||||
|
|
||||||
- [Configuring Supported Languages](./configuring-languages.md)
|
- [Configuring Supported Languages](./configuring-languages.md)
|
||||||
|
|||||||
@@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow {
|
|||||||
&should_run_tests,
|
&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![
|
let mut jobs = vec![
|
||||||
orchestrate,
|
orchestrate,
|
||||||
check_style(),
|
check_style,
|
||||||
should_run_tests.guard(run_platform_tests(Platform::Windows)),
|
should_run_tests.guard(run_platform_tests(Platform::Windows)),
|
||||||
should_run_tests.guard(run_platform_tests(Platform::Linux)),
|
should_run_tests.guard(run_tests_linux),
|
||||||
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
should_run_tests.guard(run_platform_tests(Platform::Mac)),
|
||||||
should_run_tests.guard(doctests()),
|
should_run_tests.guard(doctests()),
|
||||||
should_run_tests.guard(check_workspace_binaries()),
|
should_run_tests.guard(check_workspace_binaries()),
|
||||||
@@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow {
|
|||||||
workflow
|
workflow
|
||||||
})
|
})
|
||||||
.add_job(tests_pass.name, tests_pass.job)
|
.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
|
// Generates a bash script that checks changed files against regex patterns
|
||||||
@@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
|
|||||||
named::job(job)
|
named::job(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
|
||||||
|
|
||||||
fn check_style() -> NamedJob {
|
fn check_style() -> NamedJob {
|
||||||
fn check_for_typos() -> Step<Use> {
|
fn check_for_typos() -> Step<Use> {
|
||||||
named::uses(
|
named::uses(
|
||||||
@@ -236,15 +243,58 @@ fn check_style() -> NamedJob {
|
|||||||
.add_step(steps::checkout_repo())
|
.add_step(steps::checkout_repo())
|
||||||
.add_step(steps::cache_rust_dependencies_namespace())
|
.add_step(steps::cache_rust_dependencies_namespace())
|
||||||
.add_step(steps::setup_pnpm())
|
.add_step(steps::setup_pnpm())
|
||||||
.add_step(steps::script("./script/prettier"))
|
.add_step(steps::prettier())
|
||||||
.add_step(steps::cargo_fmt())
|
.add_step(steps::cargo_fmt())
|
||||||
.add_step(steps::trigger_autofix(false))
|
.add_step(steps::record_style_failure())
|
||||||
.add_step(steps::script("./script/check-todos"))
|
.add_step(steps::script("./script/check-todos"))
|
||||||
.add_step(steps::script("./script/check-keymaps"))
|
.add_step(steps::script("./script/check-keymaps"))
|
||||||
.add_step(check_for_typos()),
|
.add_step(check_for_typos())
|
||||||
|
.outputs([(
|
||||||
|
STYLE_FAILED_OUTPUT.to_owned(),
|
||||||
|
format!(
|
||||||
|
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||||
|
steps::RECORD_STYLE_FAILURE_STEP_ID
|
||||||
|
),
|
||||||
|
)]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
|
||||||
|
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
|
||||||
|
let clippy_failed_expr = format!(
|
||||||
|
"needs.{}.outputs.{} == 'true'",
|
||||||
|
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
|
||||||
|
);
|
||||||
|
named::bash(format!(
|
||||||
|
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
|
||||||
|
clippy_failed_expr
|
||||||
|
))
|
||||||
|
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
let style_failed_expr = format!(
|
||||||
|
"needs.{}.outputs.{} == 'true'",
|
||||||
|
check_style.name, STYLE_FAILED_OUTPUT
|
||||||
|
);
|
||||||
|
let clippy_failed_expr = format!(
|
||||||
|
"needs.{}.outputs.{} == 'true'",
|
||||||
|
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
|
||||||
|
);
|
||||||
|
let (authenticate, _token) = steps::authenticate_as_zippy();
|
||||||
|
|
||||||
|
let job = Job::default()
|
||||||
|
.runs_on(runners::LINUX_SMALL)
|
||||||
|
.cond(Expression::new(format!(
|
||||||
|
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
|
||||||
|
style_failed_expr, clippy_failed_expr
|
||||||
|
)))
|
||||||
|
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
|
||||||
|
.add_step(authenticate)
|
||||||
|
.add_step(dispatch_autofix(&run_tests_linux.name));
|
||||||
|
|
||||||
|
named::job(job)
|
||||||
|
}
|
||||||
|
|
||||||
fn check_dependencies() -> NamedJob {
|
fn check_dependencies() -> NamedJob {
|
||||||
fn install_cargo_machete() -> Step<Use> {
|
fn install_cargo_machete() -> Step<Use> {
|
||||||
named::uses(
|
named::uses(
|
||||||
@@ -305,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
|
||||||
|
|
||||||
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
||||||
let runner = match platform {
|
let runner = match platform {
|
||||||
Platform::Windows => runners::WINDOWS_DEFAULT,
|
Platform::Windows => runners::WINDOWS_DEFAULT,
|
||||||
@@ -327,12 +379,23 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
|
|||||||
.add_step(steps::setup_node())
|
.add_step(steps::setup_node())
|
||||||
.add_step(steps::clippy(platform))
|
.add_step(steps::clippy(platform))
|
||||||
.when(platform == Platform::Linux, |job| {
|
.when(platform == Platform::Linux, |job| {
|
||||||
job.add_step(steps::trigger_autofix(true))
|
job.add_step(steps::record_clippy_failure())
|
||||||
.add_step(steps::cargo_install_nextest())
|
})
|
||||||
|
.when(platform == Platform::Linux, |job| {
|
||||||
|
job.add_step(steps::cargo_install_nextest())
|
||||||
})
|
})
|
||||||
.add_step(steps::clear_target_dir_if_large(platform))
|
.add_step(steps::clear_target_dir_if_large(platform))
|
||||||
.add_step(steps::cargo_nextest(platform))
|
.add_step(steps::cargo_nextest(platform))
|
||||||
.add_step(steps::cleanup_cargo_config(platform)),
|
.add_step(steps::cleanup_cargo_config(platform))
|
||||||
|
.when(platform == Platform::Linux, |job| {
|
||||||
|
job.outputs([(
|
||||||
|
CLIPPY_FAILED_OUTPUT.to_owned(),
|
||||||
|
format!(
|
||||||
|
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
|
||||||
|
steps::RECORD_CLIPPY_FAILURE_STEP_ID
|
||||||
|
),
|
||||||
|
)])
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step<Use> {
|
|||||||
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
|
.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> {
|
pub fn cargo_fmt() -> Step<Run> {
|
||||||
named::bash("cargo fmt --all -- --check")
|
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_style_failure() -> Step<Run> {
|
||||||
|
named::bash(format!(
|
||||||
|
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||||
|
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
|
||||||
|
))
|
||||||
|
.id(RECORD_STYLE_FAILURE_STEP_ID)
|
||||||
|
.if_condition(Expression::new("always()"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cargo_install_nextest() -> Step<Use> {
|
pub fn cargo_install_nextest() -> Step<Use> {
|
||||||
@@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const CLIPPY_STEP_ID: &str = "clippy";
|
||||||
|
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
|
||||||
|
|
||||||
pub fn clippy(platform: Platform) -> Step<Run> {
|
pub fn clippy(platform: Platform) -> Step<Run> {
|
||||||
match platform {
|
match platform {
|
||||||
Platform::Windows => named::pwsh("./script/clippy.ps1"),
|
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
|
||||||
_ => named::bash("./script/clippy"),
|
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_clippy_failure() -> Step<Run> {
|
||||||
|
named::bash(format!(
|
||||||
|
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
|
||||||
|
CLIPPY_STEP_ID
|
||||||
|
))
|
||||||
|
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
|
||||||
|
.if_condition(Expression::new("always()"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
|
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
|
||||||
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
|
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
|
||||||
}
|
}
|
||||||
@@ -345,16 +374,6 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
|
|
||||||
named::bash(format!(
|
|
||||||
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}"
|
|
||||||
))
|
|
||||||
.if_condition(Expression::new(
|
|
||||||
"failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
|
|
||||||
))
|
|
||||||
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
||||||
let step = named::uses(
|
let step = named::uses(
|
||||||
"actions",
|
"actions",
|
||||||
|
|||||||
Reference in New Issue
Block a user