Compare commits
160 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd4e943597 | ||
![]() |
c5d3c7d790 | ||
![]() |
fff0ecead1 | ||
![]() |
b1b60bb7fe | ||
![]() |
0e575b2809 | ||
![]() |
65c6c709fd | ||
![]() |
858ab9cc23 | ||
![]() |
2c64b05ea4 | ||
![]() |
b7dad2cf71 | ||
![]() |
76dbcde628 | ||
![]() |
aa0f7a2d09 | ||
![]() |
372b3c7af6 | ||
![]() |
10a1140d49 | ||
![]() |
e96b68bc15 | ||
![]() |
b249593abe | ||
![]() |
c14d84cfdb | ||
![]() |
428fc6d483 | ||
![]() |
64b14ef848 | ||
![]() |
bf5ed6d1c9 | ||
![]() |
bb5cfe118f | ||
![]() |
633ce23ae9 | ||
![]() |
d43df9e841 | ||
![]() |
f8667a8379 | ||
![]() |
1460573dd4 | ||
![]() |
65de969cc8 | ||
![]() |
628a9cd8ea | ||
![]() |
ad25aba990 | ||
![]() |
99cee8778c | ||
![]() |
823a0018e5 | ||
![]() |
9cc006ff74 | ||
![]() |
0470baca50 | ||
![]() |
4605b96630 | ||
![]() |
949398cb93 | ||
![]() |
79e74b880b | ||
![]() |
59af2a7d1f | ||
![]() |
c786c0150f | ||
![]() |
5fd29d37a6 | ||
![]() |
f1204dfc33 | ||
![]() |
2e1ca47241 | ||
![]() |
5c346a4ccf | ||
![]() |
a102b08743 | ||
![]() |
2dc4f156b3 | ||
![]() |
557753d092 | ||
![]() |
65fb17e2c9 | ||
![]() |
2fe3dbed31 | ||
![]() |
fda5111dc0 | ||
![]() |
69127d2bea | ||
![]() |
db949546cf | ||
![]() |
2b5a302972 | ||
![]() |
4c0ad95acc | ||
![]() |
8c83281399 | ||
![]() |
dfc99de7b8 | ||
![]() |
fe5e81203f | ||
![]() |
c48197b280 | ||
![]() |
11545c669e | ||
![]() |
a79aef7bdd | ||
![]() |
d8bffd7ef2 | ||
![]() |
54c7d9dc5f | ||
![]() |
dd6fce6d4e | ||
![]() |
de5f87e8f2 | ||
![]() |
1b91f3de41 | ||
![]() |
19764794b7 | ||
![]() |
d49409caba | ||
![]() |
60ea4754b2 | ||
![]() |
61bc1cc441 | ||
![]() |
70575d1115 | ||
![]() |
ea42013746 | ||
![]() |
5da31fdb72 | ||
![]() |
f48a8f2b6a | ||
![]() |
d24cad30f3 | ||
![]() |
153724aad3 | ||
![]() |
bc566fe18e | ||
![]() |
91b2a84001 | ||
![]() |
e6267c42f7 | ||
![]() |
f649c31bf9 | ||
![]() |
639417c2bc | ||
![]() |
896a35f7be | ||
![]() |
4560d1ec58 | ||
![]() |
18ac4ac5ef | ||
![]() |
72bd248544 | ||
![]() |
42ae3301d0 | ||
![]() |
eb0f9ddcdc | ||
![]() |
ac9fdaa1da | ||
![]() |
8204ef1e51 | ||
![]() |
3d2fa72d1f | ||
![]() |
92bbcdeb7d | ||
![]() |
54df43e06f | ||
![]() |
4f0fad6996 | ||
![]() |
3b7c1744b4 | ||
![]() |
27a26d53b1 | ||
![]() |
d88fd00e87 | ||
![]() |
f4ba7997a7 | ||
![]() |
e360691106 | ||
![]() |
b349a8f34c | ||
![]() |
e15856a37f | ||
![]() |
852439452c | ||
![]() |
f5fd4ac670 | ||
![]() |
e1a96b68f0 | ||
![]() |
ca139b701e | ||
![]() |
eeaadc098f | ||
![]() |
18fe68d991 | ||
![]() |
a977fbc5b0 | ||
![]() |
06c0e59379 | ||
![]() |
0beb919bbb | ||
![]() |
20a0c3e920 | ||
![]() |
731b5d0def | ||
![]() |
555692fac6 | ||
![]() |
2234f91b7b | ||
![]() |
725ed5dd01 | ||
![]() |
d0583ede48 | ||
![]() |
33e05f15b2 | ||
![]() |
c1e749906f | ||
![]() |
81cb24810b | ||
![]() |
f2899bf34b | ||
![]() |
1b2ceae7ef | ||
![]() |
d166ab95a1 | ||
![]() |
b284b1a0b8 | ||
![]() |
6f32d36ec9 | ||
![]() |
190217a43b | ||
![]() |
132daef9f6 | ||
![]() |
4bee06e507 | ||
![]() |
f23314bef4 | ||
![]() |
697a39c251 | ||
![]() |
d9ea97ee9c | ||
![]() |
d8fc779a67 | ||
![]() |
001ec97c0e | ||
![]() |
2781a30971 | ||
![]() |
e0613cbd0f | ||
![]() |
1dd237139c | ||
![]() |
f63d8e4c53 | ||
![]() |
ad64a71f04 | ||
![]() |
f435af2fde | ||
![]() |
c5ee3f3e2e | ||
![]() |
7f1bd2f15e | ||
![]() |
62f2ef86dc | ||
![]() |
fda6eda3c2 | ||
![]() |
ed84767c9d | ||
![]() |
cde0a5dd27 | ||
![]() |
68f97d6069 | ||
![]() |
5dcb90858e | ||
![]() |
c731bb6d91 | ||
![]() |
4b03d791b5 | ||
![]() |
9a3e4c47d0 | ||
![]() |
568e1d0a42 | ||
![]() |
6f242772cc | ||
![]() |
8ef9ecc91f | ||
![]() |
3dd362978a | ||
![]() |
74c0ba980b | ||
![]() |
c20233e0b4 | ||
![]() |
ffb995181e | ||
![]() |
5120b6b7f9 | ||
![]() |
c9c708ff08 | ||
![]() |
9e34bb3f05 | ||
![]() |
595cf1c6c3 | ||
![]() |
d1820b183a | ||
![]() |
fb7edbfb46 | ||
![]() |
02dabbb9fa | ||
![]() |
fa8bef1496 | ||
![]() |
739e4551da | ||
![]() |
b0bef3a9a2 |
190 changed files with 10513 additions and 5534 deletions
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
|
@ -14,7 +14,7 @@ body:
|
|||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||
- Any code must be sufficient to reproduce (include context!)
|
||||
- Code must as text, not just as a screenshot.
|
||||
- Include code as text, not just as a screenshot.
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
|
|
15
.github/actionlint.yml
vendored
15
.github/actionlint.yml
vendored
|
@ -19,14 +19,27 @@ self-hosted-runner:
|
|||
- namespace-profile-16x32-ubuntu-2004-arm
|
||||
- namespace-profile-32x64-ubuntu-2004-arm
|
||||
# Namespace Ubuntu 22.04 (Everything else)
|
||||
- namespace-profile-2x4-ubuntu-2204
|
||||
- namespace-profile-4x8-ubuntu-2204
|
||||
- namespace-profile-8x16-ubuntu-2204
|
||||
- namespace-profile-16x32-ubuntu-2204
|
||||
- namespace-profile-32x64-ubuntu-2204
|
||||
# Namespace Ubuntu 24.04 (like ubuntu-latest)
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
# Namespace Limited Preview
|
||||
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||
# Self Hosted Runners
|
||||
- self-mini-macos
|
||||
- self-32vcpu-windows-2022
|
||||
|
||||
# Disable shellcheck because it doesn't like powershell
|
||||
# This should have been triggered with initial rollout of actionlint
|
||||
# but https://github.com/zed-industries/zed/pull/36693
|
||||
# somehow caused actionlint to actually check those windows jobs
|
||||
# where previously they were being skipped. Likely caused by an
|
||||
# unknown bug in actionlint where parsing of `runs-on: [ ]`
|
||||
# breaks something else. (yuck)
|
||||
paths:
|
||||
.github/workflows/{ci,release_nightly}.yml:
|
||||
ignore:
|
||||
- "shellcheck"
|
||||
|
|
2
.github/workflows/bump_collab_staging.yml
vendored
2
.github/workflows/bump_collab_staging.yml
vendored
|
@ -8,7 +8,7 @@ on:
|
|||
jobs:
|
||||
update-collab-staging-tag:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
@ -237,7 +237,7 @@ jobs:
|
|||
uses: ./.github/actions/build_docs
|
||||
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
|
||||
needs: [job_spec]
|
||||
steps:
|
||||
|
@ -418,7 +418,7 @@ jobs:
|
|||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
run: |
|
||||
|
@ -458,7 +458,7 @@ jobs:
|
|||
|
||||
tests_pass:
|
||||
name: Tests Pass
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- job_spec
|
||||
- style
|
||||
|
@ -784,7 +784,7 @@ jobs:
|
|||
bundle-windows-x64:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
needs: [windows_tests]
|
||||
|
|
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
@ -12,7 +12,7 @@ on:
|
|||
jobs:
|
||||
danger:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
|
10
.github/workflows/release_nightly.yml
vendored
10
.github/workflows/release_nightly.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
name: Run tests on Windows
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
@ -206,9 +206,6 @@ jobs:
|
|||
runs-on: github-8vcpu-ubuntu-2404
|
||||
needs: tests
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
||||
# MYTOKEN2: "value2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD remote-server
|
||||
|
@ -243,7 +240,6 @@ jobs:
|
|||
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
if: false
|
||||
needs: tests
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/nix.yml
|
||||
|
@ -252,7 +248,7 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
name: Create a Windows installer
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
needs: windows-tests
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
|
@ -294,7 +290,7 @@ jobs:
|
|||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- bundle-mac
|
||||
- bundle-linux-x86
|
||||
|
|
2
.github/workflows/script_checks.yml
vendored
2
.github/workflows/script_checks.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
shellcheck:
|
||||
name: "ShellCheck Scripts"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
|
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -1384,10 +1384,11 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rodio",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
@ -4053,6 +4054,7 @@ dependencies = [
|
|||
name = "crashes"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"crash-handler",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
|
@ -4062,6 +4064,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"system_specs",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
|
@ -4683,7 +4686,6 @@ dependencies = [
|
|||
"component",
|
||||
"ctor",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
|
@ -5720,14 +5722,10 @@ dependencies = [
|
|||
name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"editor",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"menu",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
"system_specs",
|
||||
"ui",
|
||||
"urlencoding",
|
||||
"util",
|
||||
|
@ -8470,6 +8468,7 @@ dependencies = [
|
|||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"util_macros",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
|
@ -9606,6 +9605,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"audio",
|
||||
"collections",
|
||||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
|
@ -9628,6 +9628,7 @@ dependencies = [
|
|||
"scap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
|
@ -11614,6 +11615,12 @@ dependencies = [
|
|||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pciid-parser"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
|
@ -13514,6 +13521,7 @@ dependencies = [
|
|||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
|
@ -16132,6 +16140,21 @@ dependencies = [
|
|||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system_specs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"pciid-parser",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tab_switcher"
|
||||
version = "0.1.0"
|
||||
|
@ -19766,7 +19789,6 @@ dependencies = [
|
|||
"any_vec",
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
|
@ -19785,6 +19807,7 @@ dependencies = [
|
|||
"node_runtime",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"remote",
|
||||
"schemars",
|
||||
|
@ -20373,7 +20396,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.201.4"
|
||||
version = "0.202.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
@ -20391,6 +20414,7 @@ dependencies = [
|
|||
"auto_update",
|
||||
"auto_update_ui",
|
||||
"backtrace",
|
||||
"bincode",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"channel",
|
||||
|
@ -20489,6 +20513,7 @@ dependencies = [
|
|||
"supermaven",
|
||||
"svg_preview",
|
||||
"sysinfo",
|
||||
"system_specs",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -156,6 +156,7 @@ members = [
|
|||
"crates/streaming_diff",
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/system_specs",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/tab_switcher",
|
||||
|
@ -383,6 +384,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
|||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
|
@ -451,6 +453,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
|||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
|
@ -494,6 +497,7 @@ handlebars = "4.3"
|
|||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
human_bytes = "0.4.1"
|
||||
html5ever = "0.27.0"
|
||||
http = "1.1"
|
||||
http-body = "1.0"
|
||||
|
@ -533,6 +537,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
|||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
parse_int = "0.9"
|
||||
pciid-parser = "0.8.0"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
|
@ -803,6 +808,12 @@ unexpected_cfgs = { level = "allow" }
|
|||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
|
||||
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
|
||||
# Remove when the lint gets promoted to `suspicious`.
|
||||
declare_interior_mutable_const = "deny"
|
||||
|
||||
redundant_clone = "deny"
|
||||
|
||||
# We currently do not restrict any style rules
|
||||
# as it slows down shipping code to Zed.
|
||||
#
|
||||
|
|
2
Procfile.web
Normal file
2
Procfile.web
Normal file
|
@ -0,0 +1,2 @@
|
|||
postgrest_llm: postgrest crates/collab/postgrest_llm.conf
|
||||
website: cd ../zed.dev; npm run dev -- --port=3000
|
|
@ -16,7 +16,6 @@
|
|||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
|
@ -41,7 +40,7 @@
|
|||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
|
@ -121,7 +120,7 @@
|
|||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
|
@ -856,7 +855,7 @@
|
|||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
|
@ -1195,9 +1194,16 @@
|
|||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-escape": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -915,7 +915,7 @@
|
|||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
|
@ -1301,5 +1301,12 @@
|
|||
"alt-tab": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -38,6 +38,7 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
|
|
|
@ -428,11 +428,13 @@
|
|||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
|
@ -819,7 +821,7 @@
|
|||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "project_panel::OpenWithSystem",
|
||||
"s": "workspace::OpenWithSystem",
|
||||
"z d": "project_panel::CompareMarkedFiles",
|
||||
"] c": "project_panel::SelectNextGitEntry",
|
||||
"[ c": "project_panel::SelectPrevGitEntry",
|
||||
|
|
|
@ -162,6 +162,12 @@
|
|||
// 2. Always quit the application
|
||||
// "on_last_window_closed": "quit_app",
|
||||
"on_last_window_closed": "platform_default",
|
||||
// Whether to show padding for zoomed panels.
|
||||
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
||||
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
||||
//
|
||||
// Default: true
|
||||
"zoomed_padding": true,
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
|
@ -647,6 +653,8 @@
|
|||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
|
@ -1133,11 +1141,6 @@
|
|||
// The minimum severity of the diagnostics to show inline.
|
||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||
"max_severity": null
|
||||
},
|
||||
"cargo": {
|
||||
// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
// Cargo diagnostics separately.
|
||||
"fetch_cargo_diagnostics": false
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
|
@ -1503,6 +1506,11 @@
|
|||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
// Minimum number of characters required to automatically trigger word-based completions.
|
||||
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
|
||||
//
|
||||
// Default: 3
|
||||
"words_min_length": 3,
|
||||
// Whether to fetch LSP completions or not.
|
||||
//
|
||||
// Default: true
|
||||
|
@ -1629,6 +1637,9 @@
|
|||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
|
@ -1642,9 +1653,6 @@
|
|||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
@ -1658,9 +1666,6 @@
|
|||
}
|
||||
},
|
||||
"Plain Text": {
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
|
|
|
@ -43,8 +43,8 @@
|
|||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
"shell": "system"
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
"tags": []
|
||||
// "tags": []
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1480,7 +1480,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let (start, end) = self.range.get()?;
|
||||
|
@ -1488,14 +1488,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
|||
return None;
|
||||
}
|
||||
let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
|
||||
Some(Task::ready(vec![project::Hover {
|
||||
Some(Task::ready(Some(vec![project::Hover {
|
||||
contents: vec![project::HoverBlock {
|
||||
text: "Slash commands are not supported".into(),
|
||||
kind: project::HoverBlockKind::PlainText,
|
||||
}],
|
||||
range: Some(range),
|
||||
language: None,
|
||||
}]))
|
||||
}])))
|
||||
}
|
||||
|
||||
fn inline_values(
|
||||
|
@ -1545,7 +1545,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider {
|
|||
_position: text::Anchor,
|
||||
_kind: editor::GotoDefinitionKind,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<project::LocationLink>>>> {
|
||||
) -> Option<Task<Result<Option<Vec<project::LocationLink>>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
|
@ -230,7 +230,7 @@ impl AgentConfiguration {
|
|||
let is_signed_in = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_connected()
|
||||
!workspace.client().status().borrow().is_signed_out()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
|
|
|
@ -598,17 +598,6 @@ impl AgentPanel {
|
|||
None
|
||||
};
|
||||
|
||||
// Wait for the Gemini/Native feature flag to be available.
|
||||
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
|
||||
if !client.status().borrow().is_signed_out() {
|
||||
cx.update(|_, cx| {
|
||||
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
|
||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| {
|
||||
Self::new(
|
||||
|
|
|
@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
|||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub account_too_young: bool,
|
||||
pub user_plan: Option<Plan>,
|
||||
pub tab_index: Option<isize>,
|
||||
sign_in_status: SignInStatus,
|
||||
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
account_too_young: bool,
|
||||
user_plan: Option<Plan>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
|
@ -43,6 +43,11 @@ impl AiUpsellCard {
|
|||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
|
|
|
@ -15,9 +15,10 @@ doctest = false
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
settings.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
assets: Box<dyn AssetSource>,
|
||||
}
|
||||
|
||||
struct GlobalSoundRegistry(Arc<SoundRegistry>);
|
||||
|
||||
impl Global for GlobalSoundRegistry {}
|
||||
|
||||
impl SoundRegistry {
|
||||
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
cache: Default::default(),
|
||||
assets: Box::new(source),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Arc<Self> {
|
||||
cx.global::<GlobalSoundRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
|
||||
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
|
||||
if let Some(wav) = self.cache.lock().get(name) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", name);
|
||||
let bytes = self
|
||||
.assets
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, BorrowAppContext, Global};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
|
||||
use settings::Settings;
|
||||
use std::io::Cursor;
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
mod audio_settings;
|
||||
pub use audio_settings::AudioSettings;
|
||||
|
||||
pub fn init(source: impl AssetSource, cx: &mut App) {
|
||||
SoundRegistry::set_global(source, cx);
|
||||
cx.set_global(GlobalAudio(Audio::new()));
|
||||
pub fn init(cx: &mut App) {
|
||||
AudioSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Sound {
|
||||
Joined,
|
||||
Leave,
|
||||
|
@ -38,18 +41,12 @@ impl Sound {
|
|||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output_handle: Option<OutputStream>,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
struct GlobalAudio(Audio);
|
||||
|
||||
impl Global for GlobalAudio {}
|
||||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
|
@ -58,26 +55,51 @@ impl Audio {
|
|||
self.output_handle.as_ref()
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
pub fn play_source(
|
||||
source: impl rodio::Source + Send + 'static,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<()> {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
let output_handle = this
|
||||
.ensure_output_exists()
|
||||
.ok_or_else(|| anyhow!("Could not open audio output"))?;
|
||||
output_handle.mixer().add(source);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn end_call(cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
||||
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
|
||||
if let Some(wav) = self.source_cache.get(&sound) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", sound.file());
|
||||
let bytes = cx
|
||||
.asset_source()
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.source_cache.insert(sound, source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
|
33
crates/audio/src/audio_settings.rs
Normal file
33
crates/audio/src/audio_settings.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool, // default is false
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool,
|
||||
}
|
||||
|
||||
impl Settings for AudioSettings {
|
||||
const KEY: Option<&'static str> = Some("audio");
|
||||
|
||||
type FileContent = AudioSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
|
@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
|
|||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
});
|
||||
|
||||
pub static USE_WEB_LOGIN: LazyLock<bool> = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok());
|
||||
|
||||
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
|
@ -1392,11 +1394,13 @@ impl Client {
|
|||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
if !*USE_WEB_LOGIN {
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
|
|
|
@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
|
|||
|
||||
pub static MINIDUMP_ENDPOINT: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
option_env!("ZED_MINIDUMP_ENDPOINT")
|
||||
.map(|s| s.to_owned())
|
||||
.map(str::to_string)
|
||||
.or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok())
|
||||
});
|
||||
|
||||
|
|
|
@ -46,11 +46,6 @@ impl ProjectId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct DevServerProjectId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
alter table billing_subscriptions
|
||||
add column orb_subscription_status text,
|
||||
add column orb_current_billing_period_start_date timestamp without time zone,
|
||||
add column orb_current_billing_period_end_date timestamp without time zone;
|
|
@ -400,6 +400,8 @@ impl Server {
|
|||
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
|
||||
.add_request_handler(multi_lsp_query)
|
||||
.add_request_handler(lsp_query)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
|
||||
|
@ -910,7 +912,9 @@ impl Server {
|
|||
user_id=field::Empty,
|
||||
login=field::Empty,
|
||||
impersonator=field::Empty,
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
multi_lsp_query_request=field::Empty,
|
||||
lsp_query_request=field::Empty,
|
||||
release_channel=field::Empty,
|
||||
{ TOTAL_DURATION_MS }=field::Empty,
|
||||
{ PROCESSING_DURATION_MS }=field::Empty,
|
||||
|
@ -2356,6 +2360,7 @@ where
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// todo(lsp) remove after Zed Stable hits v0.204.x
|
||||
async fn multi_lsp_query(
|
||||
request: MultiLspQuery,
|
||||
response: Response<MultiLspQuery>,
|
||||
|
@ -2366,6 +2371,21 @@ async fn multi_lsp_query(
|
|||
forward_mutating_project_request(request, response, session).await
|
||||
}
|
||||
|
||||
async fn lsp_query(
|
||||
request: proto::LspQuery,
|
||||
response: Response<proto::LspQuery>,
|
||||
session: MessageContext,
|
||||
) -> Result<()> {
|
||||
let (name, should_write) = request.query_name_and_write_permissions();
|
||||
tracing::Span::current().record("lsp_query_request", name);
|
||||
tracing::info!("lsp_query message received");
|
||||
if should_write {
|
||||
forward_mutating_project_request(request, response, session).await
|
||||
} else {
|
||||
forward_read_only_project_request(request, response, session).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify other participants that a new buffer has been created
|
||||
async fn create_buffer_for_peer(
|
||||
request: proto::CreateBufferForPeer,
|
||||
|
|
|
@ -15,13 +15,14 @@ use editor::{
|
|||
},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{StreamExt, lock::Mutex};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
|
||||
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
FakeLspAdapter,
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
};
|
||||
use lsp::LSP_REQUEST_TIMEOUT;
|
||||
use project::{
|
||||
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
||||
|
@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let command_name = "test_command";
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
code_lens_provider: Some(lsp::CodeLensOptions {
|
||||
resolve_provider: None,
|
||||
}),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec![command_name.to_string()],
|
||||
..lsp::ExecuteCommandOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"one.rs": "const ONE: usize = 1;"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
|
||||
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
|
||||
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
|
||||
(lsp_store, buffer)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let long_request_time = LSP_REQUEST_TIMEOUT / 2;
|
||||
let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
|
||||
let requests_started = Arc::new(AtomicUsize::new(0));
|
||||
let requests_completed = Arc::new(AtomicUsize::new(0));
|
||||
let _lens_requests = fake_language_server
|
||||
.set_request_handler::<lsp::request::CodeLensRequest, _, _>({
|
||||
let request_started_tx = request_started_tx.clone();
|
||||
let requests_started = requests_started.clone();
|
||||
let requests_completed = requests_completed.clone();
|
||||
move |params, cx| {
|
||||
let mut request_started_tx = request_started_tx.clone();
|
||||
let requests_started = requests_started.clone();
|
||||
let requests_completed = requests_completed.clone();
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri.as_str(),
|
||||
uri!("file:///dir/one.rs")
|
||||
);
|
||||
requests_started.fetch_add(1, atomic::Ordering::Release);
|
||||
request_started_tx.send(()).await.unwrap();
|
||||
cx.background_executor().timer(long_request_time).await;
|
||||
let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
|
||||
Ok(Some(vec![lsp::CodeLens {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
|
||||
command: Some(lsp::Command {
|
||||
title: format!("LSP Command {i}"),
|
||||
command: command_name.to_string(),
|
||||
arguments: None,
|
||||
}),
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Move cursor to a location, this should trigger the code lens call.
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([7..7])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Selection change should have initiated the first request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.forget_code_lens_task(buffer_b.read(cx).remote_id())
|
||||
.expect("Should have the fetch task started")
|
||||
});
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([1..1])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"Selection change should have initiated the second request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.forget_code_lens_task(buffer_b.read(cx).remote_id())
|
||||
.expect("Should have the fetch task started for the 2nd time")
|
||||
});
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([2..2])
|
||||
});
|
||||
});
|
||||
let () = request_started_rx.next().await.unwrap();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"Selection change should have initiated the third request"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Slow requests should be running still"
|
||||
);
|
||||
|
||||
_first_task.await.unwrap();
|
||||
_second_task.await.unwrap();
|
||||
cx_b.run_until_parked();
|
||||
assert_eq!(
|
||||
requests_started.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"No selection changes should trigger no more code lens requests"
|
||||
);
|
||||
assert_eq!(
|
||||
requests_completed.load(atomic::Ordering::Acquire),
|
||||
3,
|
||||
"After enough time, all 3 LSP requests should have been served by the language server"
|
||||
);
|
||||
let resulting_lens_actions = editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
let lsp_store = editor.project().unwrap().read(cx).lsp_store();
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.code_lens_actions(&buffer_b, cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resulting_lens_actions.len(),
|
||||
1,
|
||||
"Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
resulting_lens_actions.first().unwrap().lsp_action.title(),
|
||||
"LSP Command 3",
|
||||
"Only the final code lens action should be in the data"
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
|
|
|
@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
// the follow.
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
// Client A cycles through some tabs.
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
|||
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, window, cx);
|
||||
pane.activate_previous_item(&Default::default(), window, cx);
|
||||
});
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
|
|
@ -4850,6 +4850,7 @@ async fn test_definition(
|
|||
let definitions_1 = project_b
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(
|
||||
|
@ -4885,6 +4886,7 @@ async fn test_definition(
|
|||
let definitions_2 = project_b
|
||||
.update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions_2.len(), 1);
|
||||
|
@ -4922,6 +4924,7 @@ async fn test_definition(
|
|||
let type_definitions = project_b
|
||||
.update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(
|
||||
|
@ -5060,7 +5063,7 @@ async fn test_references(
|
|||
])))
|
||||
.unwrap();
|
||||
|
||||
let references = references.await.unwrap();
|
||||
let references = references.await.unwrap().unwrap();
|
||||
executor.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
// User is informed that a request is no longer pending.
|
||||
|
@ -5104,7 +5107,7 @@ async fn test_references(
|
|||
lsp_response_tx
|
||||
.unbounded_send(Err(anyhow!("can't find references")))
|
||||
.unwrap();
|
||||
assert_eq!(references.await.unwrap(), []);
|
||||
assert_eq!(references.await.unwrap().unwrap(), []);
|
||||
|
||||
// User is informed that the request is no longer pending.
|
||||
executor.run_until_parked();
|
||||
|
@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
|
|||
// Request hover information as the guest.
|
||||
let mut hovers = project_b
|
||||
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hovers.len(),
|
||||
2,
|
||||
|
@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
|||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let definitions = definitions.await.unwrap();
|
||||
let definitions = definitions.await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
definitions.len(),
|
||||
1,
|
||||
|
|
|
@ -2905,6 +2905,8 @@ impl CollabPanel {
|
|||
h_flex().absolute().right(rems(0.)).h_full().child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.rounded_l_sm()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.child(
|
||||
|
@ -2920,8 +2922,7 @@ impl CollabPanel {
|
|||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.join_channel_chat(channel_id, window, cx)
|
||||
}))
|
||||
.tooltip(Tooltip::text("Open channel chat"))
|
||||
.visible_on_hover(""),
|
||||
.tooltip(Tooltip::text("Open channel chat")),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::Reader)
|
||||
|
@ -2936,9 +2937,9 @@ impl CollabPanel {
|
|||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_channel_notes(channel_id, window, cx)
|
||||
}))
|
||||
.tooltip(Tooltip::text("Open channel notes"))
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
.tooltip(Tooltip::text("Open channel notes")),
|
||||
)
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
)
|
||||
.tooltip({
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use anyhow::Result;
|
||||
use db::{
|
||||
define_connection, query,
|
||||
sqlez::{bindable::Column, statement::Statement},
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::Column, domain::Domain, statement::Statement,
|
||||
thread_safe_connection::ThreadSafeConnection,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
|
||||
&[sql!(
|
||||
pub struct CommandPaletteDB(ThreadSafeConnection);
|
||||
|
||||
impl Domain for CommandPaletteDB {
|
||||
const NAME: &str = stringify!(CommandPaletteDB);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS command_invocations(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
command_name TEXT NOT NULL,
|
||||
|
@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
|
|||
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
|
||||
|
||||
impl CommandPaletteDB {
|
||||
pub async fn write_command_invocation(
|
||||
|
|
|
@ -301,6 +301,7 @@ mod tests {
|
|||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
@ -533,6 +534,7 @@ mod tests {
|
|||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
|
|
@ -6,6 +6,7 @@ edition.workspace = true
|
|||
license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
bincode.workspace = true
|
||||
crash-handler.workspace = true
|
||||
log.workspace = true
|
||||
minidumper.workspace = true
|
||||
|
@ -14,6 +15,7 @@ release_channel.workspace = true
|
|||
smol.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
system_specs.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
|
|
@ -127,6 +127,7 @@ unsafe fn suspend_all_other_threads() {
|
|||
pub struct CrashServer {
|
||||
initialization_params: OnceLock<InitCrashHandler>,
|
||||
panic_info: OnceLock<CrashPanic>,
|
||||
active_gpu: OnceLock<system_specs::GpuSpecs>,
|
||||
has_connection: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
|
@ -135,6 +136,8 @@ pub struct CrashInfo {
|
|||
pub init: InitCrashHandler,
|
||||
pub panic: Option<CrashPanic>,
|
||||
pub minidump_error: Option<String>,
|
||||
pub gpus: Vec<system_specs::GpuInfo>,
|
||||
pub active_gpu: Option<system_specs::GpuSpecs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
@ -143,7 +146,6 @@ pub struct InitCrashHandler {
|
|||
pub zed_version: String,
|
||||
pub release_channel: String,
|
||||
pub commit_sha: String,
|
||||
// pub gpu: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
@ -178,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
Err(e) => Some(format!("{e:?}")),
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
let gpus = vec![];
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
let gpus = match system_specs::read_gpu_info_from_sys_class_drm() {
|
||||
Ok(gpus) => gpus,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to collect GPU information for crash report: {err}");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
let crash_info = CrashInfo {
|
||||
init: self
|
||||
.initialization_params
|
||||
|
@ -186,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
.clone(),
|
||||
panic: self.panic_info.get().cloned(),
|
||||
minidump_error,
|
||||
active_gpu: self.active_gpu.get().cloned(),
|
||||
gpus,
|
||||
};
|
||||
|
||||
let crash_data_path = paths::logs_dir()
|
||||
|
@ -211,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
|
||||
self.panic_info.set(panic_data).expect("already panicked");
|
||||
}
|
||||
3 => {
|
||||
let gpu_specs: system_specs::GpuSpecs =
|
||||
bincode::deserialize(&buffer).expect("gpu specs");
|
||||
self.active_gpu
|
||||
.set(gpu_specs)
|
||||
.expect("already set active gpu");
|
||||
}
|
||||
_ => {
|
||||
panic!("invalid message kind");
|
||||
}
|
||||
|
@ -287,6 +310,7 @@ pub fn crash_server(socket: &Path) {
|
|||
initialization_params: OnceLock::new(),
|
||||
panic_info: OnceLock::new(),
|
||||
has_connection,
|
||||
active_gpu: OnceLock::new(),
|
||||
}),
|
||||
&shutdown,
|
||||
Some(CRASH_HANDLER_PING_TIMEOUT),
|
||||
|
|
|
@ -110,11 +110,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
|
|||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
///
|
||||
/// Arguments:
|
||||
/// - static variable name for connection
|
||||
/// - type of connection wrapper
|
||||
/// - dependencies, whose migrations should be run prior to this domain's migrations
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
macro_rules! static_connection {
|
||||
($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
|
@ -123,16 +126,6 @@ macro_rules! define_connection {
|
|||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
impl $t {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db(name: &'static str) -> Self {
|
||||
|
@ -142,7 +135,8 @@ macro_rules! define_connection {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
|
@ -153,46 +147,10 @@ macro_rules! define_connection {
|
|||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
|
||||
#[allow(unused_parens)]
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
$t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
|
||||
});
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
|
||||
let db_dir = $crate::database_dir();
|
||||
let scope = if false $(|| stringify!($global) == "global")? {
|
||||
"global"
|
||||
} else {
|
||||
$crate::RELEASE_CHANNEL.dev_name()
|
||||
};
|
||||
$t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
|
@ -219,17 +177,12 @@ mod tests {
|
|||
enum BadDB {}
|
||||
|
||||
impl Domain for BadDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(CREATE TABLE test(value);),
|
||||
// failure because test already exists
|
||||
sql!(CREATE TABLE test(value);),
|
||||
];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
@ -251,25 +204,15 @@ mod tests {
|
|||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
@ -305,25 +248,16 @@ mod tests {
|
|||
enum CorruptedDB {}
|
||||
|
||||
impl Domain for CorruptedDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests"
|
||||
}
|
||||
const NAME: &str = "db_tests";
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test(value);)]
|
||||
}
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
|
||||
}
|
||||
|
||||
enum GoodDB {}
|
||||
|
||||
impl Domain for GoodDB {
|
||||
fn name() -> &'static str {
|
||||
"db_tests" //Notice same name
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
&[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
}
|
||||
const NAME: &str = "db_tests"; //Notice same name
|
||||
const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
|
||||
}
|
||||
|
||||
let tempdir = tempfile::Builder::new()
|
||||
|
|
|
@ -2,16 +2,26 @@ use gpui::App;
|
|||
use sqlez_macros::sql;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{define_connection, query, write_and_log};
|
||||
use crate::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
write_and_log,
|
||||
};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for KeyValueStore {
|
||||
const NAME: &str = stringify!(KeyValueStore);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
|
||||
|
||||
pub trait Dismissable {
|
||||
const KEY: &'static str;
|
||||
|
@ -91,15 +101,19 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
|
||||
&[sql!(
|
||||
pub struct GlobalKeyValueStore(ThreadSafeConnection);
|
||||
|
||||
impl Domain for GlobalKeyValueStore {
|
||||
const NAME: &str = stringify!(GlobalKeyValueStore);
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
global
|
||||
);
|
||||
}
|
||||
|
||||
crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
|
||||
|
||||
impl GlobalKeyValueStore {
|
||||
query! {
|
||||
|
|
|
@ -916,7 +916,10 @@ impl RunningState {
|
|||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let is_local = project.read(cx).is_local();
|
||||
let ssh_info = project
|
||||
.read(cx)
|
||||
.ssh_client()
|
||||
.and_then(|it| it.read(cx).ssh_info());
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let DebugScenario {
|
||||
|
@ -1000,7 +1003,7 @@ impl RunningState {
|
|||
None
|
||||
};
|
||||
|
||||
let builder = ShellBuilder::new(is_local, &task.resolved.shell);
|
||||
let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell);
|
||||
let command_label = builder.command_label(&task.resolved.command_label);
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
|
|
|
@ -18,7 +18,6 @@ collections.workspace = true
|
|||
component.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
|
|
|
@ -13,7 +13,6 @@ use editor::{
|
|||
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
|
@ -24,7 +23,6 @@ use language::{
|
|||
};
|
||||
use project::{
|
||||
DiagnosticSummary, Project, ProjectPath,
|
||||
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
|
||||
project_settings::{DiagnosticSeverity, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
|
@ -79,17 +77,10 @@ pub(crate) struct ProjectDiagnosticsEditor {
|
|||
paths_to_update: BTreeSet<ProjectPath>,
|
||||
include_warnings: bool,
|
||||
update_excerpts_task: Option<Task<Result<()>>>,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
|
||||
diagnostic_summary_update: Task<()>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct CargoDiagnosticsFetchState {
|
||||
fetch_task: Option<Task<()>>,
|
||||
cancel_task: Option<Task<()>>,
|
||||
diagnostic_sources: Arc<Vec<ProjectPath>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||
|
@ -260,11 +251,7 @@ impl ProjectDiagnosticsEditor {
|
|||
)
|
||||
});
|
||||
this.diagnostics.clear();
|
||||
this.update_all_diagnostics(false, window, cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_release(&cx.entity(), |editor, _, cx| {
|
||||
editor.stop_cargo_diagnostics_fetch(cx);
|
||||
this.update_all_excerpts(window, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
@ -281,15 +268,10 @@ impl ProjectDiagnosticsEditor {
|
|||
editor,
|
||||
paths_to_update: Default::default(),
|
||||
update_excerpts_task: None,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
|
||||
fetch_task: None,
|
||||
cancel_task: None,
|
||||
diagnostic_sources: Arc::new(Vec::new()),
|
||||
},
|
||||
diagnostic_summary_update: Task::ready(()),
|
||||
_subscription: project_event_subscription,
|
||||
};
|
||||
this.update_all_diagnostics(true, window, cx);
|
||||
this.update_all_excerpts(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
|
@ -373,20 +355,10 @@ impl ProjectDiagnosticsEditor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
|
||||
if fetch_cargo_diagnostics {
|
||||
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
|
||||
self.stop_cargo_diagnostics_fetch(cx);
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
}
|
||||
} else if self.update_excerpts_task.is_some() {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
self.update_excerpts_task = None;
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
self.update_all_excerpts(window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -404,73 +376,6 @@ impl ProjectDiagnosticsEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_all_diagnostics(
|
||||
&mut self,
|
||||
first_launch: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
|
||||
if cargo_diagnostics_sources.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else if first_launch && !self.summary.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else {
|
||||
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_cargo_diagnostics(
|
||||
&mut self,
|
||||
diagnostics_sources: Arc<Vec<ProjectPath>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let project = self.project.clone();
|
||||
self.cargo_diagnostics_fetch.cancel_task = None;
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
|
||||
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
|
||||
let mut fetch_tasks = Vec::new();
|
||||
for buffer_path in diagnostics_sources.iter().cloned() {
|
||||
if cx
|
||||
.update(|cx| {
|
||||
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = join_all(fetch_tasks).await;
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.cargo_diagnostics_fetch.fetch_task = None;
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
let mut cancel_gasks = Vec::new();
|
||||
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
|
||||
.iter()
|
||||
.cloned()
|
||||
{
|
||||
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
|
||||
let _ = join_all(cancel_gasks).await;
|
||||
log::info!("Finished fetching cargo diagnostics");
|
||||
}));
|
||||
}
|
||||
|
||||
/// Enqueue an update of all excerpts. Updates all paths that either
|
||||
/// currently have diagnostics or are currently present in this view.
|
||||
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -695,30 +600,6 @@ impl ProjectDiagnosticsEditor {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
if !fetch_cargo_diagnostics {
|
||||
return Vec::new();
|
||||
}
|
||||
self.project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
|
||||
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
|
||||
entry
|
||||
.path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
== Some("rs")
|
||||
})?;
|
||||
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProjectDiagnosticsEditor {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
||||
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
|
||||
use ui::prelude::*;
|
||||
|
@ -15,26 +13,18 @@ impl Render for ToolbarControls {
|
|||
let mut include_warnings = false;
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
|
||||
editor.read(cx).cargo_diagnostics_sources(cx)
|
||||
}));
|
||||
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
|
||||
|
||||
if let Some(editor) = self.diagnostics() {
|
||||
let diagnostics = editor.read(cx);
|
||||
include_warnings = diagnostics.include_warnings;
|
||||
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
|
||||
is_updating = if fetch_cargo_diagnostics {
|
||||
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
|
||||
} else {
|
||||
diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some()
|
||||
};
|
||||
is_updating = diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some();
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
|
@ -64,7 +54,6 @@ impl Render for ToolbarControls {
|
|||
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.stop_cargo_diagnostics_fetch(cx);
|
||||
diagnostics.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
});
|
||||
|
@ -76,7 +65,7 @@ impl Render for ToolbarControls {
|
|||
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)
|
||||
.disabled(!has_stale_excerpts)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Refresh diagnostics",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
|
@ -84,17 +73,8 @@ impl Render for ToolbarControls {
|
|||
.on_click(cx.listener({
|
||||
move |toolbar_controls, _, window, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
let cargo_diagnostics_sources =
|
||||
Arc::clone(&cargo_diagnostics_sources);
|
||||
diagnostics.update(cx, move |diagnostics, cx| {
|
||||
if fetch_cargo_diagnostics {
|
||||
diagnostics.fetch_cargo_diagnostics(
|
||||
cargo_diagnostics_sources,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
}
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||
});
|
||||
|
||||
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
@ -99,6 +103,7 @@ fn handle_preprocessing() -> Result<()> {
|
|||
let mut errors = HashSet::<PreprocessorError>::new();
|
||||
|
||||
handle_frontmatter(&mut book, &mut errors);
|
||||
template_big_table_of_actions(&mut book);
|
||||
template_and_validate_keybindings(&mut book, &mut errors);
|
||||
template_and_validate_actions(&mut book, &mut errors);
|
||||
|
||||
|
@ -147,6 +152,18 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
|
|||
});
|
||||
}
|
||||
|
||||
fn template_big_table_of_actions(book: &mut Book) {
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
let needle = "{#ACTIONS_TABLE#}";
|
||||
if let Some(start) = chapter.content.rfind(needle) {
|
||||
chapter.content.replace_range(
|
||||
start..start + needle.len(),
|
||||
&generate_big_table_of_actions(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
||||
|
||||
|
@ -203,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
|||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||
"windows" => &KEYMAP_WINDOWS,
|
||||
_ => unreachable!("Not a valid OS: {}", os),
|
||||
};
|
||||
|
||||
|
@ -277,6 +295,7 @@ struct ActionDef {
|
|||
name: &'static str,
|
||||
human_name: String,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
}
|
||||
|
||||
fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
|
@ -285,6 +304,7 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
|||
name: action.name,
|
||||
human_name: command_palette::humanize_action_name(action.name),
|
||||
deprecated_aliases: action.deprecated_aliases,
|
||||
docs: action.documentation,
|
||||
})
|
||||
.collect::<Vec<ActionDef>>();
|
||||
|
||||
|
@ -418,3 +438,54 @@ fn title_regex() -> &'static Regex {
|
|||
static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
|
||||
TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
|
||||
}
|
||||
|
||||
fn generate_big_table_of_actions() -> String {
|
||||
let actions = &*ALL_ACTIONS;
|
||||
let mut output = String::new();
|
||||
|
||||
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
|
||||
for action in actions_sorted.into_iter() {
|
||||
// Add the humanized action name as the term with margin
|
||||
output.push_str(
|
||||
"<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
|
||||
);
|
||||
output.push_str(&action.human_name);
|
||||
output.push_str("</code></dt>\n");
|
||||
|
||||
// Add the definition with keymap name and description
|
||||
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
|
||||
|
||||
// Add the description, escaping HTML if needed
|
||||
if let Some(description) = action.docs {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">"),
|
||||
);
|
||||
output.push_str("<br>\n");
|
||||
}
|
||||
output.push_str("Keymap Name: <code>");
|
||||
output.push_str(action.name);
|
||||
output.push_str("</code><br>\n");
|
||||
if !action.deprecated_aliases.is_empty() {
|
||||
output.push_str("Deprecated Aliases:");
|
||||
for alias in action.deprecated_aliases.iter() {
|
||||
output.push_str("<code>");
|
||||
output.push_str(alias);
|
||||
output.push_str("</code>, ");
|
||||
}
|
||||
}
|
||||
output.push_str("\n</dd>\n");
|
||||
}
|
||||
|
||||
// Close the definition list
|
||||
output.push_str("</dl>\n");
|
||||
|
||||
output
|
||||
}
|
||||
|
|
|
@ -1898,6 +1898,60 @@ impl Editor {
|
|||
editor.update_lsp_data(false, Some(*buffer_id), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
project::Event::EntryRenamed(transaction) => {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if active_editor.entity_id() == cx.entity_id() {
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer.read(cx).as_singleton().map_or(
|
||||
false,
|
||||
|singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
let transaction = transaction.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace,
|
||||
transaction,
|
||||
"Rename".to_string(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
|
@ -2534,7 +2588,7 @@ impl Editor {
|
|||
|| binding
|
||||
.keystrokes()
|
||||
.first()
|
||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
||||
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -5520,6 +5574,11 @@ impl Editor {
|
|||
.as_ref()
|
||||
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
|
||||
|
||||
let omit_word_completions = match &query {
|
||||
Some(query) => query.chars().count() < completion_settings.words_min_length,
|
||||
None => completion_settings.words_min_length != 0,
|
||||
};
|
||||
|
||||
let (mut words, provider_responses) = match &provider {
|
||||
Some(provider) => {
|
||||
let provider_responses = provider.completions(
|
||||
|
@ -5531,9 +5590,11 @@ impl Editor {
|
|||
cx,
|
||||
);
|
||||
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx
|
||||
let words = match (omit_word_completions, completion_settings.words) {
|
||||
(true, _) | (_, WordsCompletionMode::Disabled) => {
|
||||
Task::ready(BTreeMap::default())
|
||||
}
|
||||
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
|
||||
.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
|
@ -5545,16 +5606,20 @@ impl Editor {
|
|||
|
||||
(words, provider_responses)
|
||||
}
|
||||
None => (
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
None => {
|
||||
let words = if omit_word_completions {
|
||||
Task::ready(BTreeMap::default())
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
})
|
||||
}),
|
||||
Task::ready(Ok(Vec::new())),
|
||||
),
|
||||
};
|
||||
(words, Task::ready(Ok(Vec::new())))
|
||||
}
|
||||
};
|
||||
|
||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||
|
@ -6280,7 +6345,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub async fn open_project_transaction(
|
||||
this: &WeakEntity<Editor>,
|
||||
editor: &WeakEntity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
transaction: ProjectTransaction,
|
||||
title: String,
|
||||
|
@ -6298,7 +6363,7 @@ impl Editor {
|
|||
|
||||
if let Some((buffer, transaction)) = entries.first() {
|
||||
if entries.len() == 1 {
|
||||
let excerpt = this.update(cx, |editor, cx| {
|
||||
let excerpt = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
|
@ -7621,16 +7686,16 @@ impl Editor {
|
|||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_keystroke.modifiers == modifiers
|
||||
&& accept_keystroke.modifiers.modified());
|
||||
|| (&accept_keystroke.display_modifiers == modifiers
|
||||
&& accept_keystroke.display_modifiers.modified());
|
||||
};
|
||||
if let Some(accept_partial_keystroke) = self
|
||||
.accept_edit_prediction_keybind(true, window, cx)
|
||||
.keystroke()
|
||||
{
|
||||
modifiers_held = modifiers_held
|
||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
||||
&& accept_partial_keystroke.modifiers.modified());
|
||||
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||
&& accept_partial_keystroke.display_modifiers.modified());
|
||||
}
|
||||
|
||||
if modifiers_held {
|
||||
|
@ -8979,7 +9044,7 @@ impl Editor {
|
|||
|
||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||
|
||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
||||
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
|
@ -8991,19 +9056,19 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(modifiers_color),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
true,
|
||||
)))
|
||||
.when(is_platform_style_mac, |parent| {
|
||||
parent.child(accept_keystroke.key.clone())
|
||||
parent.child(accept_keystroke.display_key.clone())
|
||||
})
|
||||
.when(!is_platform_style_mac, |parent| {
|
||||
parent.child(
|
||||
Key::new(
|
||||
util::capitalize(&accept_keystroke.key),
|
||||
util::capitalize(&accept_keystroke.display_key),
|
||||
Some(Color::Default),
|
||||
)
|
||||
.size(Some(IconSize::XSmall.rems().into())),
|
||||
|
@ -9106,7 +9171,7 @@ impl Editor {
|
|||
max_width: Pixels,
|
||||
cursor_point: Point,
|
||||
style: &EditorStyle,
|
||||
accept_keystroke: Option<&gpui::Keystroke>,
|
||||
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||
_window: &Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
|
@ -9184,7 +9249,7 @@ impl Editor {
|
|||
accept_keystroke.as_ref(),
|
||||
|el, accept_keystroke| {
|
||||
el.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(Color::Default),
|
||||
Some(IconSize::XSmall.rems().into()),
|
||||
|
@ -9254,7 +9319,7 @@ impl Editor {
|
|||
.child(completion),
|
||||
)
|
||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||
if !accept_keystroke.modifiers.modified() {
|
||||
if !accept_keystroke.display_modifiers.modified() {
|
||||
return el;
|
||||
}
|
||||
|
||||
|
@ -9273,7 +9338,7 @@ impl Editor {
|
|||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||
.child(h_flex().children(ui::render_modifiers(
|
||||
&accept_keystroke.modifiers,
|
||||
&accept_keystroke.display_modifiers,
|
||||
PlatformStyle::platform(),
|
||||
Some(if !has_completion {
|
||||
Color::Muted
|
||||
|
@ -9714,6 +9779,9 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.select_autoclose_pair(window, cx);
|
||||
|
@ -9807,6 +9875,9 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Default::default(), window, cx, |s| {
|
||||
|
@ -15659,7 +15730,9 @@ impl Editor {
|
|||
};
|
||||
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let definitions = definitions.await?;
|
||||
let Some(definitions) = definitions.await? else {
|
||||
return Ok(Navigated::No);
|
||||
};
|
||||
let navigated = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.navigate_to_hover_links(
|
||||
|
@ -16001,7 +16074,9 @@ impl Editor {
|
|||
}
|
||||
});
|
||||
|
||||
let locations = references.await?;
|
||||
let Some(locations) = references.await? else {
|
||||
return anyhow::Ok(Navigated::No);
|
||||
};
|
||||
if locations.is_empty() {
|
||||
return anyhow::Ok(Navigated::No);
|
||||
}
|
||||
|
@ -21783,7 +21858,7 @@ pub trait SemanticsProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>>;
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>>;
|
||||
|
||||
fn inline_values(
|
||||
&self,
|
||||
|
@ -21822,7 +21897,7 @@ pub trait SemanticsProvider {
|
|||
position: text::Anchor,
|
||||
kind: GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<LocationLink>>>>;
|
||||
) -> Option<Task<Result<Option<Vec<LocationLink>>>>>;
|
||||
|
||||
fn range_for_rename(
|
||||
&self,
|
||||
|
@ -21935,7 +22010,13 @@ impl CodeActionProvider for Entity<Project> {
|
|||
Ok(code_lens_actions
|
||||
.context("code lens fetch")?
|
||||
.into_iter()
|
||||
.chain(code_actions.context("code action fetch")?)
|
||||
.flatten()
|
||||
.chain(
|
||||
code_actions
|
||||
.context("code action fetch")?
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.collect())
|
||||
})
|
||||
})
|
||||
|
@ -22230,7 +22311,7 @@ impl SemanticsProvider for Entity<Project> {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
Some(self.update(cx, |project, cx| project.hover(buffer, position, cx)))
|
||||
}
|
||||
|
||||
|
@ -22251,7 +22332,7 @@ impl SemanticsProvider for Entity<Project> {
|
|||
position: text::Anchor,
|
||||
kind: GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<LocationLink>>>> {
|
||||
) -> Option<Task<Result<Option<Vec<LocationLink>>>>> {
|
||||
Some(self.update(cx, |project, cx| match kind {
|
||||
GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx),
|
||||
GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx),
|
||||
|
|
|
@ -57,7 +57,9 @@ use util::{
|
|||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -12237,6 +12239,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
|||
settings.defaults.completions = Some(CompletionSettings {
|
||||
lsp_insert_mode,
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
|
@ -12295,6 +12298,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
|||
update_test_language_settings(&mut cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
// set the opposite here to ensure that the action is overriding the default behavior
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
lsp: true,
|
||||
|
@ -12331,6 +12335,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
|||
update_test_language_settings(&mut cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
// set the opposite here to ensure that the action is overriding the default behavior
|
||||
lsp_insert_mode: LspInsertMode::Replace,
|
||||
lsp: true,
|
||||
|
@ -13072,6 +13077,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
|
|||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 10,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
@ -13168,6 +13174,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
|
|||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Enabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
@ -13231,6 +13238,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
|
|||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
words_min_length: 0,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
@ -13304,6 +13312,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
|||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
words_min_length: 0,
|
||||
lsp: false,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
|
@ -13361,6 +13370,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Enabled,
|
||||
words_min_length: 3,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::Insert,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
|
||||
cx.set_state(indoc! {"ˇ
|
||||
wow
|
||||
wowen
|
||||
wowser
|
||||
"});
|
||||
cx.simulate_keystroke("w");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if editor.context_menu.borrow_mut().is_some() {
|
||||
panic!(
|
||||
"expected completion menu to be hidden, as words completion threshold is not met"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("o");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if editor.context_menu.borrow_mut().is_some() {
|
||||
panic!(
|
||||
"expected completion menu to be hidden, as words completion threshold is not met still"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cx.simulate_keystroke("w");
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
|
||||
} else {
|
||||
panic!("expected completion menu to be open after the word completions threshold is met");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
|
||||
let position = || lsp::Position {
|
||||
line: params.text_document_position.position.line,
|
||||
|
@ -22656,7 +22715,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
|
|||
.await
|
||||
.unwrap();
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.navigate_backward(window, cx);
|
||||
pane.navigate_backward(&Default::default(), window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
pane.update(cx, |pane, cx| {
|
||||
|
@ -24243,7 +24302,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
|
|||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.navigate_backward(window, cx);
|
||||
pane.navigate_backward(&Default::default(), window, cx);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -24291,6 +24350,41 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
register_project_item::<Editor>(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root1", json!({})).await;
|
||||
fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/root1".as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let handle = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project_path = (worktree_id, "one.pdf");
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any().entity_type(),
|
||||
TypeId::of::<InvalidBufferView>()
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
|
||||
editor
|
||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
|||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
|
@ -74,6 +74,7 @@ use std::{
|
|||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
ops::{Deref, Range},
|
||||
path::{self, Path},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
|
@ -89,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||
use util::post_inc;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{
|
||||
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||
notifications::NotifyTaskExt,
|
||||
CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
|
||||
item::Item, notifications::NotifyTaskExt,
|
||||
};
|
||||
|
||||
/// Determines what kinds of highlights should be applied to a lines background.
|
||||
|
@ -3602,171 +3603,187 @@ impl EditorElement {
|
|||
let focus_handle = editor.focus_handle(cx);
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let header =
|
||||
div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
let header = div()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.pl_0p5()
|
||||
.pr_5()
|
||||
.rounded_sm()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
{
|
||||
colors.border_focused
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
.map(|header| {
|
||||
let editor = self.editor.clone();
|
||||
let buffer_id = for_excerpt.buffer_id;
|
||||
let toggle_chevron_icon =
|
||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||
header.child(
|
||||
div()
|
||||
.hover(|style| style.bg(colors.element_selected))
|
||||
.rounded_xs()
|
||||
.child(
|
||||
ButtonLike::new("toggle-buffer-fold")
|
||||
.style(ui::ButtonStyle::Transparent)
|
||||
.height(px(28.).into())
|
||||
.width(px(28.))
|
||||
.children(toggle_chevron_icon)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta_in(
|
||||
"Toggle Excerpt Fold",
|
||||
Some(&ToggleFold),
|
||||
"Alt+click to toggle all",
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |event, window, cx| {
|
||||
if event.modifiers().alt {
|
||||
// Alt+click toggles all buffers
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_fold_all(
|
||||
&ToggleFoldAll,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
// Regular click toggles single buffer
|
||||
if is_folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_buffer(buffer_id, cx);
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_buffer(buffer_id, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.children(indicator)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.single_line()
|
||||
.when_some(file_status, |el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| el.strikethrough())
|
||||
}),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
})
|
||||
.children(
|
||||
editor
|
||||
.addons
|
||||
.values()
|
||||
.filter_map(|addon| {
|
||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size(Pixels(12.0))
|
||||
.justify_center()
|
||||
.children(indicator),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.map(|path_header| {
|
||||
let filename = filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into());
|
||||
|
||||
path_header
|
||||
.when(ItemSettings::get_global(cx).file_icons, |el| {
|
||||
let path = path::Path::new(filename.as_str());
|
||||
let icon = FileIcons::get_icon(path, cx)
|
||||
.unwrap_or_default();
|
||||
let icon =
|
||||
Icon::from_path(icon).color(Color::Muted);
|
||||
el.child(icon)
|
||||
})
|
||||
.child(Label::new(filename).single_line().when_some(
|
||||
file_status,
|
||||
|el, status| {
|
||||
el.color(if status.is_conflicted() {
|
||||
Color::Conflict
|
||||
} else if status.is_modified() {
|
||||
Color::Modified
|
||||
} else if status.is_deleted() {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Created
|
||||
})
|
||||
.when(status.is_deleted(), |el| {
|
||||
el.strikethrough()
|
||||
})
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
})
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||
colors.text_disabled
|
||||
} else {
|
||||
colors.text_muted
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||
|el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.id("jump-to-file-button")
|
||||
.gap_2p5()
|
||||
.child(Label::new("Jump To File"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenExcerpts,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
let file = for_excerpt.buffer.file().cloned();
|
||||
let editor = self.editor.clone();
|
||||
|
@ -3782,25 +3799,31 @@ impl EditorElement {
|
|||
&& let Some(worktree) =
|
||||
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
|
||||
{
|
||||
let worktree = worktree.read(cx);
|
||||
let relative_path = file.path();
|
||||
let entry_for_path = worktree.read(cx).entry_for_path(relative_path);
|
||||
let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref());
|
||||
let has_relative_path =
|
||||
worktree.read(cx).root_entry().is_some_and(Entry::is_dir);
|
||||
let entry_for_path = worktree.entry_for_path(relative_path);
|
||||
let abs_path = entry_for_path.map(|e| {
|
||||
e.canonical_path.as_deref().map_or_else(
|
||||
|| worktree.abs_path().join(relative_path),
|
||||
Path::to_path_buf,
|
||||
)
|
||||
});
|
||||
let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir);
|
||||
|
||||
let parent_abs_path =
|
||||
abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let parent_abs_path = abs_path
|
||||
.as_ref()
|
||||
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let relative_path = has_relative_path
|
||||
.then_some(relative_path)
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
let visible_in_project_panel =
|
||||
relative_path.is_some() && worktree.read(cx).is_visible();
|
||||
relative_path.is_some() && worktree.is_visible();
|
||||
let reveal_in_project_panel = entry_for_path
|
||||
.filter(|_| visible_in_project_panel)
|
||||
.map(|entry| entry.id);
|
||||
menu = menu
|
||||
.when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| {
|
||||
.when_some(abs_path, |menu, abs_path| {
|
||||
menu.entry(
|
||||
"Copy Path",
|
||||
Some(Box::new(zed_actions::workspace::CopyPath)),
|
||||
|
@ -7127,7 +7150,7 @@ fn header_jump_data(
|
|||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||
|
||||
impl AcceptEditPredictionBinding {
|
||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
||||
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||
if let Some(binding) = self.0.as_ref() {
|
||||
match &binding.keystrokes() {
|
||||
[keystroke, ..] => Some(keystroke),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{Editor, RangeToAnchorExt};
|
||||
use gpui::{Context, Window};
|
||||
use gpui::{Context, HighlightStyle, Window};
|
||||
use language::CursorShape;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
enum MatchingBracketHighlight {}
|
||||
|
||||
|
@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights(
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
|
||||
editor.clear_highlights::<MatchingBracketHighlight>(cx);
|
||||
|
||||
let newest_selection = editor.selections.newest::<usize>(cx);
|
||||
// Don't highlight brackets if the selection isn't empty
|
||||
|
@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights(
|
|||
.buffer_snapshot
|
||||
.innermost_enclosing_bracket_ranges(head..tail, None)
|
||||
{
|
||||
editor.highlight_background::<MatchingBracketHighlight>(
|
||||
&[
|
||||
editor.highlight_text::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
opening_range.to_anchors(&snapshot.buffer_snapshot),
|
||||
closing_range.to_anchors(&snapshot.buffer_snapshot),
|
||||
],
|
||||
|theme| theme.colors().editor_document_highlight_bracket_background,
|
||||
HighlightStyle {
|
||||
background_color: Some(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_document_highlight_bracket_background,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
@ -104,7 +112,7 @@ mod tests {
|
|||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test«(»"Test argument"«)» {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
|
@ -115,7 +123,7 @@ mod tests {
|
|||
another_test(1, ˇ2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test«(»1, 2, 3«)»;
|
||||
}
|
||||
|
@ -126,7 +134,7 @@ mod tests {
|
|||
anotherˇ_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") «{»
|
||||
another_test(1, 2, 3);
|
||||
«}»
|
||||
|
@ -138,7 +146,7 @@ mod tests {
|
|||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
|
@ -150,8 +158,8 @@ mod tests {
|
|||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test("Test argument") {
|
||||
cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
|
||||
pub fn test«("Test argument") {
|
||||
another_test(1, 2, 3);
|
||||
}
|
||||
"#});
|
||||
|
|
|
@ -562,7 +562,7 @@ pub fn show_link_definition(
|
|||
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
|
||||
})?;
|
||||
if let Some(task) = task {
|
||||
task.await.ok().map(|definition_result| {
|
||||
task.await.ok().flatten().map(|definition_result| {
|
||||
(
|
||||
definition_result.iter().find_map(|link| {
|
||||
link.origin.as_ref().and_then(|origin| {
|
||||
|
|
|
@ -428,7 +428,7 @@ fn show_hover(
|
|||
};
|
||||
|
||||
let hovers_response = if let Some(hover_request) = hover_request {
|
||||
hover_request.await
|
||||
hover_request.await.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
|
|||
use util::{ResultExt, TryFutureExt, paths::PathExt};
|
||||
use workspace::{
|
||||
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
invalid_buffer_view::InvalidBufferView,
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
};
|
||||
|
@ -1401,6 +1402,16 @@ impl ProjectItem for Editor {
|
|||
|
||||
editor
|
||||
}
|
||||
|
||||
fn for_broken_project_item(
|
||||
abs_path: &Path,
|
||||
is_local: bool,
|
||||
e: &anyhow::Error,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InvalidBufferView> {
|
||||
Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_ranges<'a>(
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
|
||||
use db::sqlez::statement::Statement;
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
domain::Domain,
|
||||
statement::Statement,
|
||||
},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use fs::MTime;
|
||||
use itertools::Itertools as _;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
|
@ -83,7 +87,11 @@ impl Column for SerializedEditor {
|
|||
}
|
||||
}
|
||||
|
||||
define_connection!(
|
||||
pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
|
||||
|
||||
impl Domain for EditorDb {
|
||||
const NAME: &str = stringify!(EditorDb);
|
||||
|
||||
// Current schema shape using pseudo-rust syntax:
|
||||
// editors(
|
||||
// item_id: usize,
|
||||
|
@ -113,7 +121,8 @@ define_connection!(
|
|||
// start: usize,
|
||||
// end: usize,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
|
@ -189,7 +198,9 @@ define_connection!(
|
|||
) STRICT;
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
db::static_connection!(DB, EditorDb, [WorkspaceDb]);
|
||||
|
||||
// https://www.sqlite.org/limits.html
|
||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||
|
|
|
@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
) -> Option<Task<Option<Vec<project::Hover>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.hover(&buffer, position, cx)
|
||||
}
|
||||
|
@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
|
|||
position: text::Anchor,
|
||||
kind: crate::GotoDefinitionKind,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
|
||||
) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
|
||||
let buffer = self.to_base(buffer, &[position], cx)?;
|
||||
self.0.definitions(&buffer, position, kind, cx)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool {
|
|||
}
|
||||
|
||||
pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
|
||||
if editor.read(cx).project().is_some_and(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_server_statuses(cx)
|
||||
.any(|(_, status)| status.name == RUST_ANALYZER_NAME)
|
||||
}) {
|
||||
register_action(editor, window, cancel_flycheck_action);
|
||||
register_action(editor, window, run_flycheck_action);
|
||||
register_action(editor, window, clear_flycheck_action);
|
||||
}
|
||||
|
||||
if editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
|
@ -38,9 +49,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
|
|||
register_action(editor, window, go_to_parent_module);
|
||||
register_action(editor, window, expand_macro_recursively);
|
||||
register_action(editor, window, open_docs);
|
||||
register_action(editor, window, cancel_flycheck_action);
|
||||
register_action(editor, window, run_flycheck_action);
|
||||
register_action(editor, window, clear_flycheck_action);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -309,7 +317,7 @@ fn cancel_flycheck_action(
|
|||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
|
@ -321,10 +329,7 @@ fn cancel_flycheck_action(
|
|||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
|
@ -337,7 +342,7 @@ fn run_flycheck_action(
|
|||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
|
@ -349,10 +354,7 @@ fn run_flycheck_action(
|
|||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
|
@ -365,7 +367,7 @@ fn clear_flycheck_action(
|
|||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
let buffer_id = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
|
@ -377,9 +379,6 @@ fn clear_flycheck_action(
|
|||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
});
|
||||
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
|
|
@ -182,7 +182,9 @@ impl Editor {
|
|||
let signature_help = task.await;
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some(mut signature_help) = signature_help.into_iter().next() else {
|
||||
let Some(mut signature_help) =
|
||||
signature_help.unwrap_or_default().into_iter().next()
|
||||
else {
|
||||
editor
|
||||
.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
|
|
|
@ -98,6 +98,10 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag {
|
|||
// integration too, and we'd like to turn Gemini/Native on in new builds
|
||||
// without enabling Claude Code in old builds.
|
||||
const NAME: &'static str = "gemini-and-native";
|
||||
|
||||
fn enabled_for_all() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeFeatureFlag;
|
||||
|
@ -201,7 +205,7 @@ impl FeatureFlagAppExt for App {
|
|||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlags>()
|
||||
.map(|flags| flags.has_flag::<T>())
|
||||
.unwrap_or(false)
|
||||
.unwrap_or(T::enabled_for_all())
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
|
|
|
@ -15,13 +15,9 @@ path = "src/feedback.rs"
|
|||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
client.workspace = true
|
||||
gpui.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
menu.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
sysinfo.workspace = true
|
||||
system_specs.workspace = true
|
||||
ui.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
use gpui::{App, ClipboardItem, PromptLevel, actions};
|
||||
use system_specs::SystemSpecs;
|
||||
use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::feedback::FileBugReport;
|
||||
|
||||
pub mod feedback_modal;
|
||||
|
||||
pub mod system_specs;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
/// Copies system specifications to the clipboard for bug reports.
|
||||
CopySystemSpecsIntoClipboard,
|
||||
/// Opens email client to send feedback to Zed support.
|
||||
EmailZed,
|
||||
/// Opens the Zed repository on GitHub.
|
||||
|
|
|
@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
#[cfg(windows)]
|
||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||
#[cfg(not(windows))]
|
||||
let raw_query = raw_query.trim().to_owned();
|
||||
let raw_query = raw_query.trim();
|
||||
|
||||
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||
let raw_query = raw_query.trim_end_matches(':').to_owned();
|
||||
let path = path_position.path.to_str();
|
||||
let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
|
||||
let file_query_end = if path_trimmed == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
Some(path.unwrap().len())
|
||||
};
|
||||
|
||||
let query = FileSearchQuery {
|
||||
|
|
|
@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
|||
" ndan ",
|
||||
" band ",
|
||||
"a bandana",
|
||||
"bandana:",
|
||||
] {
|
||||
picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
|
@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a": {
|
||||
"foo:bar.rs": "",
|
||||
"foo.rs": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, _, cx) = build_find_picker(project, cx);
|
||||
|
||||
// 'foo:' matches both files
|
||||
cx.simulate_input("foo:");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
|
||||
// 'foo:b' matches one of the files
|
||||
cx.simulate_input("b");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 2);
|
||||
assert_match_at_position(picker, 0, "foo:bar.rs");
|
||||
});
|
||||
|
||||
cx.dispatch_action(editor::actions::Backspace);
|
||||
|
||||
// 'foo:1' matches both files, specifying which row to jump to
|
||||
cx.simulate_input("1");
|
||||
picker.update(cx, |picker, _| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_match_at_position(picker, 0, "foo.rs");
|
||||
assert_match_at_position(picker, 1, "foo:bar.rs");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unicode_paths(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
|||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
|
@ -263,6 +263,7 @@ pub struct App {
|
|||
pub(crate) focus_handles: Arc<FocusMap>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
|
@ -312,6 +313,7 @@ impl App {
|
|||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
|
@ -337,6 +339,7 @@ impl App {
|
|||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
keyboard_mapper,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
|
@ -376,6 +379,7 @@ impl App {
|
|||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
|
@ -424,6 +428,11 @@ impl App {
|
|||
self.keyboard_layout.as_ref()
|
||||
}
|
||||
|
||||
/// Get the current keyboard mapper.
|
||||
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
||||
&self.keyboard_mapper
|
||||
}
|
||||
|
||||
/// Invokes a handler when the current keyboard layout changes
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
|
|
|
@ -352,7 +352,7 @@ impl<T> Flatten<T> for Result<T> {
|
|||
}
|
||||
|
||||
/// Information about the GPU GPUI is running on.
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct GpuSpecs {
|
||||
/// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
|
||||
pub is_software_emulated: bool,
|
||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
|||
pub use binding::*;
|
||||
pub use context::*;
|
||||
|
||||
use crate::{Action, Keystroke, is_no_action};
|
||||
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||
use collections::{HashMap, HashSet};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
|||
/// only.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
input: &[impl AsKeystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||
|
@ -192,7 +192,6 @@ impl Keymap {
|
|||
|
||||
(bindings, !pending.is_empty())
|
||||
}
|
||||
|
||||
/// Check if the given binding is enabled, given a certain key context.
|
||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||
|
@ -639,7 +638,7 @@ mod tests {
|
|||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||
let actual = keymap
|
||||
.bindings_for_action(action)
|
||||
.map(|binding| binding.keystrokes[0].unparse())
|
||||
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "{:?}", action);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use crate::{
|
||||
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
|
@ -32,7 +33,15 @@ impl KeyBinding {
|
|||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context_predicate =
|
||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||
Self::load(
|
||||
keystrokes,
|
||||
Box::new(action),
|
||||
context_predicate,
|
||||
false,
|
||||
None,
|
||||
&DummyKeyboardMapper,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
|
@ -40,24 +49,22 @@ impl KeyBinding {
|
|||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
use_key_equivalents: bool,
|
||||
action_input: Option<SharedString>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.map(|source| {
|
||||
let keystroke = Keystroke::parse(source)?;
|
||||
Ok(KeybindingKeystroke::new(
|
||||
keystroke,
|
||||
use_key_equivalents,
|
||||
keyboard_mapper,
|
||||
))
|
||||
})
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1
|
||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
||||
{
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
|
@ -79,13 +86,13 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Check if the given keystrokes match this binding.
|
||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
||||
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||
if self.keystrokes.len() < typed.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||
if !typed.should_match(target) {
|
||||
if !typed.as_keystroke().should_match(target) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +101,7 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Get the keystrokes associated with this binding
|
||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
||||
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||
self.keystrokes.as_slice()
|
||||
}
|
||||
|
||||
|
|
|
@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
|
|||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
|
@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
|
|||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
|
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
|
|||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
use collections::HashMap;
|
||||
|
||||
use crate::{KeybindingKeystroke, Keystroke};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
|
@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
|
|||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// A trait for platform-specific keyboard mappings
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// Map a key equivalent to its platform-specific representation
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke;
|
||||
/// Get the key equivalents for the current keyboard layout,
|
||||
/// only used on macOS
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||
}
|
||||
|
||||
/// A dummy implementation of the platform keyboard mapper
|
||||
pub struct DummyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
keystroke: Keystroke,
|
||||
_use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
KeybindingKeystroke::from_keystroke(keystroke)
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,14 @@ use std::{
|
|||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
use crate::PlatformKeyboardMapper;
|
||||
|
||||
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||
pub trait AsKeystroke {
|
||||
/// Returns the GPUI representation of the keystroke.
|
||||
fn as_keystroke(&self) -> &Keystroke;
|
||||
}
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
pub struct Keystroke {
|
||||
|
@ -24,6 +32,17 @@ pub struct Keystroke {
|
|||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeybindingKeystroke {
|
||||
/// The GPUI representation of the keystroke.
|
||||
pub inner: Keystroke,
|
||||
/// The modifiers to display.
|
||||
pub display_modifiers: Modifiers,
|
||||
/// The key to display.
|
||||
pub display_key: String,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
|
@ -58,7 +77,7 @@ impl Keystroke {
|
|||
///
|
||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||
/// both possibilities for self against the target.
|
||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
||||
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Some(key_char) = self
|
||||
.key_char
|
||||
|
@ -71,7 +90,7 @@ impl Keystroke {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -83,12 +102,12 @@ impl Keystroke {
|
|||
.filter(|key_char| key_char != &&self.key)
|
||||
{
|
||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
||||
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
target.modifiers == self.modifiers && target.key == self.key
|
||||
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||
}
|
||||
|
||||
/// key syntax is:
|
||||
|
@ -200,31 +219,7 @@ impl Keystroke {
|
|||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
let mut str = String::new();
|
||||
if self.modifiers.function {
|
||||
str.push_str("fn-");
|
||||
}
|
||||
if self.modifiers.control {
|
||||
str.push_str("ctrl-");
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
str.push_str("alt-");
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
str.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
str.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
str.push_str("win-");
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
str.push_str("shift-");
|
||||
}
|
||||
str.push_str(&self.key);
|
||||
str
|
||||
unparse(&self.modifiers, &self.key)
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
|
@ -266,6 +261,32 @@ impl Keystroke {
|
|||
}
|
||||
}
|
||||
|
||||
impl KeybindingKeystroke {
|
||||
/// Create a new keybinding keystroke from the given keystroke
|
||||
pub fn new(
|
||||
inner: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> Self {
|
||||
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||
}
|
||||
|
||||
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||
let key = keystroke.key.clone();
|
||||
let modifiers = keystroke.modifiers;
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
unparse(&self.display_modifiers, &self.display_key)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_printable_key(key: &str) -> bool {
|
||||
!matches!(
|
||||
key,
|
||||
|
@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
|
|||
|
||||
impl std::fmt::Display for Keystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
display_modifiers(&self.modifiers, f)?;
|
||||
display_key(&self.key, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
let key = match self.key.as_str() {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
impl std::fmt::Display for KeybindingKeystroke {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
display_modifiers(&self.display_modifiers, f)?;
|
||||
display_key(&self.display_key, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,3 +571,110 @@ pub struct Capslock {
|
|||
#[serde(default)]
|
||||
pub on: bool,
|
||||
}
|
||||
|
||||
impl AsKeystroke for Keystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsKeystroke for KeybindingKeystroke {
|
||||
fn as_keystroke(&self) -> &Keystroke {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if modifiers.control {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('^')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "ctrl-")?;
|
||||
}
|
||||
if modifiers.alt {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌥')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "alt-")?;
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⌘')?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
f.write_char('❖')?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
f.write_char('⊞')?;
|
||||
}
|
||||
if modifiers.shift {
|
||||
#[cfg(target_os = "macos")]
|
||||
f.write_char('⇧')?;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
write!(f, "shift-")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let key = match key {
|
||||
#[cfg(target_os = "macos")]
|
||||
"backspace" => '⌫',
|
||||
#[cfg(target_os = "macos")]
|
||||
"up" => '↑',
|
||||
#[cfg(target_os = "macos")]
|
||||
"down" => '↓',
|
||||
#[cfg(target_os = "macos")]
|
||||
"left" => '←',
|
||||
#[cfg(target_os = "macos")]
|
||||
"right" => '→',
|
||||
#[cfg(target_os = "macos")]
|
||||
"tab" => '⇥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"escape" => '⎋',
|
||||
#[cfg(target_os = "macos")]
|
||||
"shift" => '⇧',
|
||||
#[cfg(target_os = "macos")]
|
||||
"control" => '⌃',
|
||||
#[cfg(target_os = "macos")]
|
||||
"alt" => '⌥',
|
||||
#[cfg(target_os = "macos")]
|
||||
"platform" => '⌘',
|
||||
|
||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||
key => return f.write_str(key),
|
||||
};
|
||||
f.write_char(key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||
let mut result = String::new();
|
||||
if modifiers.function {
|
||||
result.push_str("fn-");
|
||||
}
|
||||
if modifiers.control {
|
||||
result.push_str("ctrl-");
|
||||
}
|
||||
if modifiers.alt {
|
||||
result.push_str("alt-");
|
||||
}
|
||||
if modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
result.push_str("cmd-");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
result.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
result.push_str("win-");
|
||||
}
|
||||
if modifiers.shift {
|
||||
result.push_str("shift-");
|
||||
}
|
||||
result.push_str(&key);
|
||||
result
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
|||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
|
@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
self.keyboard_layout()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(crate::DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
use super::{
|
||||
BoolExt, MacKeyboardLayout,
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
renderer,
|
||||
|
@ -8,8 +8,9 @@ use crate::{
|
|||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||
hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
|
@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
|
|||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
|
@ -189,6 +191,9 @@ impl MacPlatform {
|
|||
#[cfg(not(feature = "font-kit"))]
|
||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
|
||||
Self(Mutex::new(MacPlatformState {
|
||||
headless,
|
||||
text_system,
|
||||
|
@ -209,6 +214,7 @@ impl MacPlatform {
|
|||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
keyboard_mapper,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -348,19 +354,19 @@ impl MacPlatform {
|
|||
let mut mask = NSEventModifierFlags::empty();
|
||||
for (modifier, flag) in &[
|
||||
(
|
||||
keystroke.modifiers.platform,
|
||||
keystroke.display_modifiers.platform,
|
||||
NSEventModifierFlags::NSCommandKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.control,
|
||||
keystroke.display_modifiers.control,
|
||||
NSEventModifierFlags::NSControlKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.alt,
|
||||
keystroke.display_modifiers.alt,
|
||||
NSEventModifierFlags::NSAlternateKeyMask,
|
||||
),
|
||||
(
|
||||
keystroke.modifiers.shift,
|
||||
keystroke.display_modifiers.shift,
|
||||
NSEventModifierFlags::NSShiftKeyMask,
|
||||
),
|
||||
] {
|
||||
|
@ -373,7 +379,7 @@ impl MacPlatform {
|
|||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||
|
@ -882,6 +888,10 @@ impl Platform for MacPlatform {
|
|||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
self.0.lock().keyboard_mapper.clone()
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
|
@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
|||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
let keyboard_layout = MacKeyboardLayout::new();
|
||||
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
|
@ -237,6 +238,10 @@ impl Platform for TestPlatform {
|
|||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(DummyKeyboardMapper)
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
|
|
|
@ -9,10 +9,8 @@ use parking::Parker;
|
|||
use parking_lot::Mutex;
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Foundation::TimeSpan,
|
||||
System::Threading::{
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
|
||||
WorkItemPriority,
|
||||
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::{LPARAM, WPARAM},
|
||||
|
@ -56,12 +54,7 @@ impl WindowsDispatcher {
|
|||
Ok(())
|
||||
})
|
||||
};
|
||||
ThreadPool::RunWithPriorityAndOptionsAsync(
|
||||
&handler,
|
||||
WorkItemPriority::High,
|
||||
WorkItemOptions::TimeSliced,
|
||||
)
|
||||
.log_err();
|
||||
ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
|
||||
}
|
||||
|
||||
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
|
||||
|
@ -72,12 +65,7 @@ impl WindowsDispatcher {
|
|||
Ok(())
|
||||
})
|
||||
};
|
||||
let delay = TimeSpan {
|
||||
// A time period expressed in 100-nanosecond units.
|
||||
// 10,000,000 ticks per second
|
||||
Duration: (duration.as_nanos() / 100) as i64,
|
||||
};
|
||||
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
|
||||
ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||
use crate::{
|
||||
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsKeyboardMapper {
|
||||
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||
vkey_to_key: HashMap<u16, String>,
|
||||
vkey_to_shifted: HashMap<u16, String>,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
|
@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
|||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||
fn map_key_equivalent(
|
||||
&self,
|
||||
mut keystroke: Keystroke,
|
||||
use_key_equivalents: bool,
|
||||
) -> KeybindingKeystroke {
|
||||
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||
else {
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
if shifted_key && keystroke.modifiers.shift {
|
||||
log::warn!(
|
||||
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||
keystroke.key
|
||||
);
|
||||
}
|
||||
|
||||
let shift = shifted_key || keystroke.modifiers.shift;
|
||||
keystroke.modifiers.shift = false;
|
||||
|
||||
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map key equivalent '{:?}' to a valid key",
|
||||
keystroke
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
|
||||
keystroke.key = if shift {
|
||||
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||
log::error!(
|
||||
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||
keystroke,
|
||||
vkey
|
||||
);
|
||||
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||
};
|
||||
shifted_key
|
||||
} else {
|
||||
key.clone()
|
||||
};
|
||||
|
||||
let modifiers = Modifiers {
|
||||
shift,
|
||||
..keystroke.modifiers
|
||||
};
|
||||
|
||||
KeybindingKeystroke {
|
||||
inner: keystroke,
|
||||
display_modifiers: modifiers,
|
||||
display_key: key,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardLayout {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||
|
@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
|
|||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut key_to_vkey = HashMap::default();
|
||||
let mut vkey_to_key = HashMap::default();
|
||||
let mut vkey_to_shifted = HashMap::default();
|
||||
for vkey in CANDIDATE_VKEYS {
|
||||
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||
vkey_to_key.insert(vkey.0, key);
|
||||
}
|
||||
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||
if scan_code == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
key_to_vkey,
|
||||
vkey_to_key,
|
||||
vkey_to_shifted,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||
if use_key_equivalents {
|
||||
get_vkey_from_key_with_us_layout(key)
|
||||
} else {
|
||||
self.key_to_vkey.get(key).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_keystroke_key(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
|
@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||
match key {
|
||||
// ` => VK_OEM_3
|
||||
"`" => Some((VK_OEM_3.0, false)),
|
||||
"~" => Some((VK_OEM_3.0, true)),
|
||||
"1" => Some((VK_1.0, false)),
|
||||
"!" => Some((VK_1.0, true)),
|
||||
"2" => Some((VK_2.0, false)),
|
||||
"@" => Some((VK_2.0, true)),
|
||||
"3" => Some((VK_3.0, false)),
|
||||
"#" => Some((VK_3.0, true)),
|
||||
"4" => Some((VK_4.0, false)),
|
||||
"$" => Some((VK_4.0, true)),
|
||||
"5" => Some((VK_5.0, false)),
|
||||
"%" => Some((VK_5.0, true)),
|
||||
"6" => Some((VK_6.0, false)),
|
||||
"^" => Some((VK_6.0, true)),
|
||||
"7" => Some((VK_7.0, false)),
|
||||
"&" => Some((VK_7.0, true)),
|
||||
"8" => Some((VK_8.0, false)),
|
||||
"*" => Some((VK_8.0, true)),
|
||||
"9" => Some((VK_9.0, false)),
|
||||
"(" => Some((VK_9.0, true)),
|
||||
"0" => Some((VK_0.0, false)),
|
||||
")" => Some((VK_0.0, true)),
|
||||
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||
"[" => Some((VK_OEM_4.0, false)),
|
||||
"{" => Some((VK_OEM_4.0, true)),
|
||||
"]" => Some((VK_OEM_6.0, false)),
|
||||
"}" => Some((VK_OEM_6.0, true)),
|
||||
"\\" => Some((VK_OEM_5.0, false)),
|
||||
"|" => Some((VK_OEM_5.0, true)),
|
||||
";" => Some((VK_OEM_1.0, false)),
|
||||
":" => Some((VK_OEM_1.0, true)),
|
||||
"'" => Some((VK_OEM_7.0, false)),
|
||||
"\"" => Some((VK_OEM_7.0, true)),
|
||||
"," => Some((VK_OEM_COMMA.0, false)),
|
||||
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||
"/" => Some((VK_OEM_2.0, false)),
|
||||
"?" => Some((VK_OEM_2.0, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||
VK_OEM_3,
|
||||
VK_OEM_MINUS,
|
||||
VK_OEM_PLUS,
|
||||
VK_OEM_4,
|
||||
VK_OEM_5,
|
||||
VK_OEM_6,
|
||||
VK_OEM_1,
|
||||
VK_OEM_7,
|
||||
VK_OEM_COMMA,
|
||||
VK_OEM_PERIOD,
|
||||
VK_OEM_2,
|
||||
VK_OEM_102,
|
||||
VK_OEM_8,
|
||||
VK_ABNT_C1,
|
||||
VK_0,
|
||||
VK_1,
|
||||
VK_2,
|
||||
VK_3,
|
||||
VK_4,
|
||||
VK_5,
|
||||
VK_6,
|
||||
VK_7,
|
||||
VK_8,
|
||||
VK_9,
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_mapper() {
|
||||
let mapper = WindowsKeyboardMapper::new();
|
||||
|
||||
// Normal case
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "a");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||
|
||||
// Shifted case, ctrl-$
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||
assert_eq!(mapped.inner, keystroke);
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Shifted case, but shift is true
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "$".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
|
||||
// Windows style
|
||||
let keystroke = Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: "4".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||
assert_eq!(mapped.inner.key, "$");
|
||||
assert_eq!(mapped.display_key, "4");
|
||||
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::OsStr,
|
||||
mem::ManuallyDrop,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
|
@ -350,6 +351,10 @@ impl Platform for WindowsPlatform {
|
|||
)
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||
Rc::new(WindowsKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||
}
|
||||
|
@ -460,13 +465,15 @@ impl Platform for WindowsPlatform {
|
|||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
if url.is_empty() {
|
||||
return;
|
||||
}
|
||||
let url_string = url.to_string();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
if url_string.is_empty() {
|
||||
return;
|
||||
}
|
||||
open_target(url_string.as_str());
|
||||
open_target(&url_string)
|
||||
.with_context(|| format!("Opening url: {}", url_string))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -514,37 +521,29 @@ impl Platform for WindowsPlatform {
|
|||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
let Ok(file_full_path) = path.canonicalize() else {
|
||||
log::error!("unable to parse file path");
|
||||
if path.as_os_str().is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let path = path.to_path_buf();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(path) = file_full_path.to_str() else {
|
||||
return;
|
||||
};
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
open_target_in_explorer(path);
|
||||
open_target_in_explorer(&path)
|
||||
.with_context(|| format!("Revealing path {} in explorer", path.display()))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_with_system(&self, path: &Path) {
|
||||
let Ok(full_path) = path.canonicalize() else {
|
||||
log::error!("unable to parse file full path: {}", path.display());
|
||||
if path.as_os_str().is_empty() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let path = path.to_path_buf();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(full_path_str) = full_path.to_str() else {
|
||||
return;
|
||||
};
|
||||
if full_path_str.is_empty() {
|
||||
return;
|
||||
};
|
||||
open_target(full_path_str);
|
||||
open_target(&path)
|
||||
.with_context(|| format!("Opening {} with system", path.display()))
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -735,39 +734,67 @@ pub(crate) struct WindowCreationInfo {
|
|||
pub(crate) disable_direct_composition: bool,
|
||||
}
|
||||
|
||||
fn open_target(target: &str) {
|
||||
unsafe {
|
||||
let ret = ShellExecuteW(
|
||||
fn open_target(target: impl AsRef<OsStr>) -> Result<()> {
|
||||
let target = target.as_ref();
|
||||
let ret = unsafe {
|
||||
ShellExecuteW(
|
||||
None,
|
||||
windows::core::w!("open"),
|
||||
&HSTRING::from(target),
|
||||
None,
|
||||
None,
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
if ret.0 as isize <= 32 {
|
||||
log::error!("Unable to open target: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
)
|
||||
};
|
||||
if ret.0 as isize <= 32 {
|
||||
Err(anyhow::anyhow!(
|
||||
"Unable to open target: {}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn open_target_in_explorer(target: &str) {
|
||||
fn open_target_in_explorer(target: &Path) -> Result<()> {
|
||||
let dir = target.parent().context("No parent folder found")?;
|
||||
let desktop = unsafe { SHGetDesktopFolder()? };
|
||||
|
||||
let mut dir_item = std::ptr::null_mut();
|
||||
unsafe {
|
||||
let ret = ShellExecuteW(
|
||||
desktop.ParseDisplayName(
|
||||
HWND::default(),
|
||||
None,
|
||||
windows::core::w!("open"),
|
||||
windows::core::w!("explorer.exe"),
|
||||
&HSTRING::from(format!("/select,{}", target).as_str()),
|
||||
&HSTRING::from(dir),
|
||||
None,
|
||||
SW_SHOWDEFAULT,
|
||||
);
|
||||
if ret.0 as isize <= 32 {
|
||||
log::error!(
|
||||
"Unable to open target in explorer: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
&mut dir_item,
|
||||
std::ptr::null_mut(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut file_item = std::ptr::null_mut();
|
||||
unsafe {
|
||||
desktop.ParseDisplayName(
|
||||
HWND::default(),
|
||||
None,
|
||||
&HSTRING::from(target),
|
||||
None,
|
||||
&mut file_item,
|
||||
std::ptr::null_mut(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let highlight = [file_item as *const _];
|
||||
unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| {
|
||||
if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
|
||||
// On some systems, the above call mysteriously fails with "file not
|
||||
// found" even though the file is there. In these cases, ShellExecute()
|
||||
// seems to work as a fallback (although it won't select the file).
|
||||
open_target(dir).context("Opening target parent folder")
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Can not open target path: {}", err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn file_open_dialog(
|
||||
|
|
|
@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
|
|||
mod persistence {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::{define_connection, query, sqlez_macros::sql};
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection! {
|
||||
pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
pub struct ImageViewerDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ImageViewerDb {
|
||||
const NAME: &str = stringify!(ImageViewerDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE image_viewers (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
|
@ -417,9 +424,11 @@ mod persistence {
|
|||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
|
||||
|
||||
impl ImageViewerDb {
|
||||
query! {
|
||||
pub async fn save_image_path(
|
||||
|
|
|
@ -24,6 +24,7 @@ serde_json_lenient.workspace = true
|
|||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
util_macros.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
|
@ -25,7 +25,7 @@ use util::split_str_with_ranges;
|
|||
|
||||
/// Path used for unsaved buffer that contains style json. To support the json language server, this
|
||||
/// matches the name used in the generated schemas.
|
||||
const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
|
||||
const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json");
|
||||
|
||||
pub(crate) struct DivInspector {
|
||||
state: State,
|
||||
|
|
|
@ -1569,11 +1569,21 @@ impl Buffer {
|
|||
self.send_operation(op, true, cx);
|
||||
}
|
||||
|
||||
pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
|
||||
let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
|
||||
return None;
|
||||
};
|
||||
Some(&self.diagnostics[idx].1)
|
||||
pub fn buffer_diagnostics(
|
||||
&self,
|
||||
for_server: Option<LanguageServerId>,
|
||||
) -> Vec<&DiagnosticEntry<Anchor>> {
|
||||
match for_server {
|
||||
Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
|
||||
Ok(idx) => self.diagnostics[idx].1.iter().collect(),
|
||||
Err(_) => Vec::new(),
|
||||
},
|
||||
None => self
|
||||
.diagnostics
|
||||
.iter()
|
||||
.flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
|
||||
|
|
|
@ -5,7 +5,7 @@ use anyhow::Result;
|
|||
use collections::{FxHashMap, HashMap, HashSet};
|
||||
use ec4rs::{
|
||||
Properties as EditorconfigProperties,
|
||||
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
|
||||
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
|
||||
};
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{App, Modifiers};
|
||||
|
@ -350,6 +350,12 @@ pub struct CompletionSettings {
|
|||
/// Default: `fallback`
|
||||
#[serde(default = "default_words_completion_mode")]
|
||||
pub words: WordsCompletionMode,
|
||||
/// How many characters has to be in the completions query to automatically show the words-based completions.
|
||||
/// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
|
||||
///
|
||||
/// Default: 3
|
||||
#[serde(default = "default_3")]
|
||||
pub words_min_length: usize,
|
||||
/// Whether to fetch LSP completions or not.
|
||||
///
|
||||
/// Default: true
|
||||
|
@ -359,7 +365,7 @@ pub struct CompletionSettings {
|
|||
/// When set to 0, waits indefinitely.
|
||||
///
|
||||
/// Default: 0
|
||||
#[serde(default = "default_lsp_fetch_timeout_ms")]
|
||||
#[serde(default)]
|
||||
pub lsp_fetch_timeout_ms: u64,
|
||||
/// Controls how LSP completions are inserted.
|
||||
///
|
||||
|
@ -405,8 +411,8 @@ fn default_lsp_insert_mode() -> LspInsertMode {
|
|||
LspInsertMode::ReplaceSuffix
|
||||
}
|
||||
|
||||
fn default_lsp_fetch_timeout_ms() -> u64 {
|
||||
0
|
||||
fn default_3() -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
|
@ -1131,6 +1137,10 @@ impl AllLanguageSettings {
|
|||
}
|
||||
|
||||
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
|
||||
let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
|
||||
MaxLineLen::Value(u) => Some(u as u32),
|
||||
MaxLineLen::Off => None,
|
||||
});
|
||||
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
|
||||
IndentSize::Value(u) => NonZeroU32::new(u as u32),
|
||||
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
|
||||
|
@ -1158,6 +1168,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
|
|||
*target = value;
|
||||
}
|
||||
}
|
||||
merge(&mut settings.preferred_line_length, preferred_line_length);
|
||||
merge(&mut settings.tab_size, tab_size);
|
||||
merge(&mut settings.hard_tabs, hard_tabs);
|
||||
merge(
|
||||
|
@ -1463,6 +1474,7 @@ impl settings::Settings for AllLanguageSettings {
|
|||
} else {
|
||||
d.completions = Some(CompletionSettings {
|
||||
words: mode,
|
||||
words_min_length: 3,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
lsp_insert_mode: LspInsertMode::ReplaceSuffix,
|
||||
|
|
|
@ -96,7 +96,7 @@ impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
|
|||
}
|
||||
|
||||
type DefaultIndex = usize;
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ToolchainList {
|
||||
pub toolchains: Vec<Toolchain>,
|
||||
pub default: Option<DefaultIndex>,
|
||||
|
|
|
@ -4,7 +4,6 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use settings::get_key_equivalents;
|
||||
use ui::{Button, ButtonStyle};
|
||||
use ui::{
|
||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||
|
@ -169,7 +168,8 @@ impl Item for KeyContextView {
|
|||
impl Render for KeyContextView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
||||
|
||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
|
|
|
@ -1743,6 +1743,5 @@ pub enum Event {
|
|||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for LspLogView {}
|
||||
impl EventEmitter<EditorEvent> for LspLogView {}
|
||||
impl EventEmitter<SearchEvent> for LspLogView {}
|
||||
|
|
|
@ -231,6 +231,7 @@
|
|||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"module"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
|
@ -250,4 +251,4 @@
|
|||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||
(jsx_text) @text.jsx
|
||||
(jsx_text) @text.jsx
|
||||
|
|
|
@ -11,6 +11,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string) @injection.content
|
||||
|
|
|
@ -510,20 +510,6 @@ impl LspAdapter for RustLspAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
if cargo_diagnostics_fetched_separately {
|
||||
let disable_check_on_save = json!({
|
||||
"checkOnSave": false,
|
||||
});
|
||||
if let Some(initialization_options) = &mut original.initialization_options {
|
||||
merge_json_value_into(disable_check_on_save, initialization_options);
|
||||
} else {
|
||||
original.initialization_options = Some(disable_check_on_save);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(original)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,6 +237,7 @@
|
|||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"module"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
|
@ -256,4 +257,4 @@
|
|||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||
(jsx_text) @text.jsx
|
||||
(jsx_text) @text.jsx
|
||||
|
|
|
@ -11,6 +11,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
|
|
|
@ -248,6 +248,7 @@
|
|||
"is"
|
||||
"keyof"
|
||||
"let"
|
||||
"module"
|
||||
"namespace"
|
||||
"new"
|
||||
"of"
|
||||
|
@ -272,4 +273,4 @@
|
|||
"while"
|
||||
"with"
|
||||
"yield"
|
||||
] @keyword
|
||||
] @keyword
|
||||
|
|
|
@ -15,6 +15,21 @@
|
|||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_obj (#eq? @_obj "styled")
|
||||
property: (property_identifier))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (call_expression
|
||||
function: (identifier) @_name (#eq? @_name "styled"))
|
||||
arguments: (template_string (string_fragment) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
)
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @_name (#eq? @_name "html")
|
||||
arguments: (template_string) @injection.content
|
||||
|
|
|
@ -25,6 +25,7 @@ async-trait.workspace = true
|
|||
collections.workspace = true
|
||||
cpal.workspace = true
|
||||
futures.workspace = true
|
||||
audio.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
|
||||
gpui_tokio.workspace = true
|
||||
http_client_tls.workspace = true
|
||||
|
@ -35,6 +36,7 @@ nanoid.workspace = true
|
|||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
smallvec.workspace = true
|
||||
settings.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -24,8 +24,11 @@ mod livekit_client;
|
|||
)))]
|
||||
pub use livekit_client::*;
|
||||
|
||||
// If you need proper LSP in livekit_client you've got to comment out
|
||||
// the mocks and test
|
||||
// If you need proper LSP in livekit_client you've got to comment
|
||||
// - the cfg blocks above
|
||||
// - the mods: mock_client & test and their conditional blocks
|
||||
// - the pub use mock_client::* and their conditional blocks
|
||||
|
||||
#[cfg(any(
|
||||
test,
|
||||
feature = "test-support",
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use audio::AudioSettings;
|
||||
use collections::HashMap;
|
||||
use futures::{SinkExt, channel::mpsc};
|
||||
use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
|
||||
use gpui_tokio::Tokio;
|
||||
use log::info;
|
||||
use playback::capture_local_video_track;
|
||||
use settings::Settings;
|
||||
|
||||
mod playback;
|
||||
#[cfg(feature = "record-microphone")]
|
||||
mod record;
|
||||
|
||||
use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication};
|
||||
pub use playback::AudioStream;
|
||||
|
@ -125,9 +126,14 @@ impl Room {
|
|||
pub fn play_remote_audio_track(
|
||||
&self,
|
||||
track: &RemoteAudioTrack,
|
||||
_cx: &App,
|
||||
cx: &mut App,
|
||||
) -> Result<playback::AudioStream> {
|
||||
Ok(self.playback.play_remote_audio_track(&track.0))
|
||||
if AudioSettings::get_global(cx).rodio_audio {
|
||||
info!("Using experimental.rodio_audio audio pipeline");
|
||||
playback::play_remote_audio_track(&track.0, cx)
|
||||
} else {
|
||||
Ok(self.playback.play_remote_audio_track(&track.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,13 +18,16 @@ use livekit::webrtc::{
|
|||
video_stream::native::NativeVideoStream,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use rodio::Source;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Weak;
|
||||
use std::sync::atomic::{self, AtomicI32};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
mod source;
|
||||
|
||||
pub(crate) struct AudioStack {
|
||||
executor: BackgroundExecutor,
|
||||
apm: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
|
@ -40,6 +43,29 @@ pub(crate) struct AudioStack {
|
|||
const SAMPLE_RATE: u32 = 48000;
|
||||
const NUM_CHANNELS: u32 = 2;
|
||||
|
||||
pub(crate) fn play_remote_audio_track(
|
||||
track: &livekit::track::RemoteAudioTrack,
|
||||
cx: &mut gpui::App,
|
||||
) -> Result<AudioStream> {
|
||||
let stop_handle = Arc::new(AtomicBool::new(false));
|
||||
let stop_handle_clone = stop_handle.clone();
|
||||
let stream = source::LiveKitStream::new(cx.background_executor(), track)
|
||||
.stoppable()
|
||||
.periodic_access(Duration::from_millis(50), move |s| {
|
||||
if stop_handle.load(Ordering::Relaxed) {
|
||||
s.stop();
|
||||
}
|
||||
});
|
||||
audio::Audio::play_source(stream, cx).context("Could not play audio")?;
|
||||
|
||||
let on_drop = util::defer(move || {
|
||||
stop_handle_clone.store(true, Ordering::Relaxed);
|
||||
});
|
||||
Ok(AudioStream::Output {
|
||||
_drop: Box::new(on_drop),
|
||||
})
|
||||
}
|
||||
|
||||
impl AudioStack {
|
||||
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
||||
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||
|
@ -61,7 +87,7 @@ impl AudioStack {
|
|||
) -> AudioStream {
|
||||
let output_task = self.start_output();
|
||||
|
||||
let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
|
||||
let source = AudioMixerSource {
|
||||
ssrc: next_ssrc,
|
||||
sample_rate: SAMPLE_RATE,
|
||||
|
@ -97,6 +123,23 @@ impl AudioStack {
|
|||
}
|
||||
}
|
||||
|
||||
fn start_output(&self) -> Arc<Task<()>> {
|
||||
if let Some(task) = self._output_task.borrow().upgrade() {
|
||||
return task;
|
||||
}
|
||||
let task = Arc::new(self.executor.spawn({
|
||||
let apm = self.apm.clone();
|
||||
let mixer = self.mixer.clone();
|
||||
async move {
|
||||
Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
*self._output_task.borrow_mut() = Arc::downgrade(&task);
|
||||
task
|
||||
}
|
||||
|
||||
pub(crate) fn capture_local_microphone_track(
|
||||
&self,
|
||||
) -> Result<(crate::LocalAudioTrack, AudioStream)> {
|
||||
|
@ -139,23 +182,6 @@ impl AudioStack {
|
|||
))
|
||||
}
|
||||
|
||||
fn start_output(&self) -> Arc<Task<()>> {
|
||||
if let Some(task) = self._output_task.borrow().upgrade() {
|
||||
return task;
|
||||
}
|
||||
let task = Arc::new(self.executor.spawn({
|
||||
let apm = self.apm.clone();
|
||||
let mixer = self.mixer.clone();
|
||||
async move {
|
||||
Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
*self._output_task.borrow_mut() = Arc::downgrade(&task);
|
||||
task
|
||||
}
|
||||
|
||||
async fn play_output(
|
||||
apm: Arc<Mutex<apm::AudioProcessingModule>>,
|
||||
mixer: Arc<Mutex<audio_mixer::AudioMixer>>,
|
||||
|
|
67
crates/livekit_client/src/livekit_client/playback/source.rs
Normal file
67
crates/livekit_client/src/livekit_client/playback/source.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use futures::StreamExt;
|
||||
use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
|
||||
use livekit::track::RemoteAudioTrack;
|
||||
use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter};
|
||||
|
||||
use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
|
||||
fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
|
||||
let samples = frame.data.iter().copied();
|
||||
let samples = SampleTypeConverter::<_, _>::new(samples);
|
||||
let samples: Vec<f32> = samples.collect();
|
||||
SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples)
|
||||
}
|
||||
|
||||
pub struct LiveKitStream {
|
||||
// shared_buffer: SharedBuffer,
|
||||
inner: rodio::queue::SourcesQueueOutput,
|
||||
_receiver_task: gpui::Task<()>,
|
||||
}
|
||||
|
||||
impl LiveKitStream {
|
||||
pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
|
||||
let mut stream =
|
||||
NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32);
|
||||
let (queue_input, queue_output) = rodio::queue::queue(true);
|
||||
// spawn rtc stream
|
||||
let receiver_task = executor.spawn({
|
||||
async move {
|
||||
while let Some(frame) = stream.next().await {
|
||||
let samples = frame_to_samplesbuffer(frame);
|
||||
queue_input.append(samples);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
LiveKitStream {
|
||||
_receiver_task: receiver_task,
|
||||
inner: queue_output,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LiveKitStream {
|
||||
type Item = rodio::Sample;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for LiveKitStream {
|
||||
fn current_span_len(&self) -> Option<usize> {
|
||||
self.inner.current_span_len()
|
||||
}
|
||||
|
||||
fn channels(&self) -> rodio::ChannelCount {
|
||||
self.inner.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> rodio::SampleRate {
|
||||
self.inner.sample_rate()
|
||||
}
|
||||
|
||||
fn total_duration(&self) -> Option<std::time::Duration> {
|
||||
self.inner.total_duration()
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
|
|||
const JSON_RPC_VERSION: &str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
||||
|
||||
const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
|
||||
|
|
|
@ -835,7 +835,7 @@ impl MultiBuffer {
|
|||
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
|
||||
drop(snapshot);
|
||||
|
||||
let mut buffer_ids = Vec::new();
|
||||
let mut buffer_ids = Vec::with_capacity(buffer_edits.len());
|
||||
for (buffer_id, mut edits) in buffer_edits {
|
||||
buffer_ids.push(buffer_id);
|
||||
edits.sort_by_key(|edit| edit.range.start);
|
||||
|
|
|
@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page(
|
|||
v_flex()
|
||||
.mt_2()
|
||||
.gap_6()
|
||||
.child({
|
||||
let mut ai_upsell_card =
|
||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
||||
|
||||
ai_upsell_card.tab_index = Some({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
});
|
||||
|
||||
ai_upsell_card
|
||||
})
|
||||
.child(
|
||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
||||
.tab_index(Some({
|
||||
tab_index += 1;
|
||||
tab_index - 1
|
||||
})),
|
||||
)
|
||||
.child(render_llm_provider_section(
|
||||
&mut tab_index,
|
||||
workspace,
|
||||
|
|
|
@ -606,7 +606,7 @@ fn render_popular_settings_section(
|
|||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
const LIGATURE_TOOLTIP: &str =
|
||||
"Font ligatures combine two characters into one. For example, turning =/= into ≠.";
|
||||
"Font ligatures combine two characters into one. For example, turning != into ≠.";
|
||||
|
||||
v_flex()
|
||||
.pt_6()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue