Compare commits
259 Commits
v0.89.0-pr
...
v0.91.3-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707bd7c156 | ||
|
|
656f68dc69 | ||
|
|
186334bb12 | ||
|
|
b1324ebe1f | ||
|
|
e9aec1d67a | ||
|
|
32ed547c2c | ||
|
|
39915f7c96 | ||
|
|
2c2cea1c92 | ||
|
|
75b1f60126 | ||
|
|
2b8b954c3e | ||
|
|
4efe62b3e5 | ||
|
|
049c987310 | ||
|
|
56b0bf8601 | ||
|
|
1aa1774688 | ||
|
|
f8b9417406 | ||
|
|
75ad76bfb2 | ||
|
|
7dab17e233 | ||
|
|
27c83ca3f7 | ||
|
|
9b7617403d | ||
|
|
ea5d677ef8 | ||
|
|
adc8337ad4 | ||
|
|
8ad7eb7598 | ||
|
|
b365e48ff0 | ||
|
|
77367bf2e4 | ||
|
|
5b6d1a27ff | ||
|
|
c17dbab6f1 | ||
|
|
b272db9e21 | ||
|
|
aedef7bc58 | ||
|
|
1cd11bfe66 | ||
|
|
0db0a1ccef | ||
|
|
6e5de2fbbb | ||
|
|
00cede63a8 | ||
|
|
2842fc2b1d | ||
|
|
2ae8b558b9 | ||
|
|
5e68dc5c92 | ||
|
|
5821bc4161 | ||
|
|
d8a2e176e6 | ||
|
|
df76ab98cf | ||
|
|
01bd5c30fc | ||
|
|
ac1882b99b | ||
|
|
04e43899c0 | ||
|
|
8542911eec | ||
|
|
018466171b | ||
|
|
594b9def20 | ||
|
|
b4f3a88b38 | ||
|
|
56b749788f | ||
|
|
e969e3b028 | ||
|
|
086cfe57c5 | ||
|
|
3d02f7ce5f | ||
|
|
7db690b713 | ||
|
|
db5bb4ec03 | ||
|
|
0b3b732310 | ||
|
|
56a4c2afae | ||
|
|
d8c1ab9c68 | ||
|
|
c1f1ee6b05 | ||
|
|
b2bdca4779 | ||
|
|
097632467d | ||
|
|
87efd25d42 | ||
|
|
bb65d75798 | ||
|
|
9cbb63d374 | ||
|
|
5bef2f1778 | ||
|
|
fb83ab8e9f | ||
|
|
738b06a778 | ||
|
|
4213cc013c | ||
|
|
6ce3f3bf27 | ||
|
|
a8d43c6d71 | ||
|
|
7deddd1149 | ||
|
|
57ff173e29 | ||
|
|
85b049f250 | ||
|
|
ddcbc774ab | ||
|
|
e4cbc29f98 | ||
|
|
6304897abc | ||
|
|
3719c206c9 | ||
|
|
91e1bb8fd4 | ||
|
|
a7f06f962b | ||
|
|
e0dd9e4185 | ||
|
|
11dbbcc9dd | ||
|
|
999b2365a8 | ||
|
|
e3f319467a | ||
|
|
908de23b72 | ||
|
|
afaff7f9a9 | ||
|
|
817644eb20 | ||
|
|
e2f46d5448 | ||
|
|
16e3e04501 | ||
|
|
1e43fec1c5 | ||
|
|
e996a66596 | ||
|
|
a75e9faa83 | ||
|
|
c8a9d73ea6 | ||
|
|
d4192fc3e9 | ||
|
|
8216d26a7a | ||
|
|
fc1f8c5657 | ||
|
|
8ca1a7d43d | ||
|
|
66f215cd13 | ||
|
|
9e9d8e3a7b | ||
|
|
9d58c4526d | ||
|
|
5f143f689f | ||
|
|
572d40381a | ||
|
|
2c5e83bf72 | ||
|
|
78f9642ac2 | ||
|
|
cd63ec2c7f | ||
|
|
03a96d2793 | ||
|
|
0ac7a3bc21 | ||
|
|
28ba27c9c5 | ||
|
|
34e134fafb | ||
|
|
351e4863cd | ||
|
|
11ab1a8cc6 | ||
|
|
be8d268eb9 | ||
|
|
72372ddf0e | ||
|
|
86ec43c908 | ||
|
|
29de420b59 | ||
|
|
6269cec4f1 | ||
|
|
6067575e38 | ||
|
|
f56d642b88 | ||
|
|
8882b22c9c | ||
|
|
e94129446d | ||
|
|
b1f009cdce | ||
|
|
4c405e65a3 | ||
|
|
0ad76ac92c | ||
|
|
02c1efc60d | ||
|
|
f5d1f314e0 | ||
|
|
d2b8501347 | ||
|
|
d5441ba386 | ||
|
|
d3e0d38bef | ||
|
|
d26cc2c897 | ||
|
|
43500dbf60 | ||
|
|
0dae8f2dd8 | ||
|
|
a6feaf1300 | ||
|
|
c93b6cc599 | ||
|
|
e8479f23f9 | ||
|
|
6f2726524e | ||
|
|
fccbac4887 | ||
|
|
0d90c6d02e | ||
|
|
4b9a3c66e6 | ||
|
|
7aeaa84657 | ||
|
|
8dc679e74e | ||
|
|
6737ee1495 | ||
|
|
cc63d3d048 | ||
|
|
a9f865d828 | ||
|
|
dfd72770e7 | ||
|
|
3fc2e0754b | ||
|
|
bdd3e77e02 | ||
|
|
7bfb51ee76 | ||
|
|
559a58d737 | ||
|
|
c1c91dc2e3 | ||
|
|
572c59eec4 | ||
|
|
17560cc5b0 | ||
|
|
dbbd0558c3 | ||
|
|
2003d3dbe4 | ||
|
|
7a78e64831 | ||
|
|
16090c35ae | ||
|
|
ef7ec265c8 | ||
|
|
53906fd3da | ||
|
|
ac7178068f | ||
|
|
cfcfc3bf6b | ||
|
|
093ce8a9ac | ||
|
|
7b066df7e6 | ||
|
|
a0e2e5db7d | ||
|
|
2b1aeb07bc | ||
|
|
9c59146026 | ||
|
|
69b8267b6b | ||
|
|
ada222078c | ||
|
|
f4f060667e | ||
|
|
337dda8e3a | ||
|
|
8032324470 | ||
|
|
e46d1549d6 | ||
|
|
23836eb251 | ||
|
|
296a0bf510 | ||
|
|
cb975f1252 | ||
|
|
0949ee84d8 | ||
|
|
a2d58068a7 | ||
|
|
c12bdc894a | ||
|
|
398b0f303c | ||
|
|
3d1ba1b363 | ||
|
|
571151173c | ||
|
|
12dd91c89c | ||
|
|
5e4da6433f | ||
|
|
624467ebca | ||
|
|
9a13a2ba2c | ||
|
|
7fbafc8030 | ||
|
|
311074e397 | ||
|
|
49c5a3fa86 | ||
|
|
2190a27dff | ||
|
|
70c5489c13 | ||
|
|
4ac5f7b14e | ||
|
|
e56fcd69b5 | ||
|
|
4f3165692f | ||
|
|
917d8949b7 | ||
|
|
6b89243902 | ||
|
|
0ed8bbc818 | ||
|
|
c872f581d1 | ||
|
|
bef6932da7 | ||
|
|
5790d6993e | ||
|
|
a89f3ed445 | ||
|
|
7c60f636d5 | ||
|
|
ca077408d7 | ||
|
|
2f97c7a4f1 | ||
|
|
e377459948 | ||
|
|
99a0e11e70 | ||
|
|
46d2cbaa4c | ||
|
|
33c4c32196 | ||
|
|
7417835f06 | ||
|
|
f6a4706410 | ||
|
|
00265c19a0 | ||
|
|
345fad3e9d | ||
|
|
f00f16fe37 | ||
|
|
f97999d97f | ||
|
|
5fbbc1936f | ||
|
|
b38f760fcd | ||
|
|
d3ed958308 | ||
|
|
6b00db75ad | ||
|
|
56ecfaf2f0 | ||
|
|
3750e64d9f | ||
|
|
571d2f4966 | ||
|
|
a81d164ea6 | ||
|
|
d0aff65b1c | ||
|
|
55c8c6d3fb | ||
|
|
cf934ab696 | ||
|
|
20e65a533c | ||
|
|
3768851799 | ||
|
|
c55aee84d3 | ||
|
|
cc055901e1 | ||
|
|
5e43dcaab8 | ||
|
|
0bd9d5b1fa | ||
|
|
585d13d3db | ||
|
|
a55eafa726 | ||
|
|
e67e6e6f70 | ||
|
|
6d24a8a763 | ||
|
|
0065f5715c | ||
|
|
9d6b3744f7 | ||
|
|
40c6baf7cb | ||
|
|
d64dc3960d | ||
|
|
2390815d67 | ||
|
|
2ffbeca7dd | ||
|
|
51c82da840 | ||
|
|
d8ce333cf8 | ||
|
|
01621972c5 | ||
|
|
e1a6dc9077 | ||
|
|
788f97ec68 | ||
|
|
ae7606ce21 | ||
|
|
0d281c1b89 | ||
|
|
0dd7694ff5 | ||
|
|
03a351fb26 | ||
|
|
38078b93cc | ||
|
|
a2ab7c9eb9 | ||
|
|
4717ce1da3 | ||
|
|
22e4086658 | ||
|
|
eeba72d775 | ||
|
|
8f95435548 | ||
|
|
69e8a166e4 | ||
|
|
dc365472a6 | ||
|
|
ed0fa2404c | ||
|
|
89446c7fd4 | ||
|
|
890c42a75a | ||
|
|
52e8bf2928 | ||
|
|
404bebab63 | ||
|
|
ffbfbe422b | ||
|
|
3904971bd8 | ||
|
|
8f6e67f440 | ||
|
|
80080a43e4 |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -93,7 +93,6 @@ jobs:
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
|
||||
4
.github/workflows/randomized_tests.yml
vendored
4
.github/workflows/randomized_tests.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- randomized-tests-runner
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
# schedule:
|
||||
# - cron: '0 * * * *'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
16
.github/workflows/release_actions.yml
vendored
16
.github/workflows/release_actions.yml
vendored
@@ -21,19 +21,3 @@ jobs:
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
mixpanel_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10.5"
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/mixpanel_release/requirements.txt
|
||||
- run: >
|
||||
python script/mixpanel_release/main.py
|
||||
${{ github.event.release.tag_name }}
|
||||
${{ secrets.MIXPANEL_PROJECT_ID }}
|
||||
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_USERNAME }}
|
||||
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_SECRET }}
|
||||
|
||||
252
Cargo.lock
generated
252
Cargo.lock
generated
@@ -100,15 +100,24 @@ name = "ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"chrono",
|
||||
"collections",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"language",
|
||||
"menu",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"tiktoken-rs",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -180,15 +189,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.71"
|
||||
@@ -219,15 +219,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"rust-embed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.4.1"
|
||||
@@ -404,7 +395,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,7 +443,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -495,7 +486,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -715,27 +706,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.59.2"
|
||||
version = "0.65.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
|
||||
checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"clap 2.34.0",
|
||||
"env_logger 0.9.3",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.18",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -861,6 +867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1085,21 +1093,6 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim 0.8.0",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
@@ -1112,9 +1105,9 @@ dependencies = [
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"strsim 0.10.0",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap 0.16.0",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1144,7 +1137,7 @@ name = "cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 3.2.25",
|
||||
"clap",
|
||||
"core-foundation",
|
||||
"core-services",
|
||||
"dirs 3.0.2",
|
||||
@@ -1246,7 +1239,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.12.4"
|
||||
version = "0.14.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -1254,7 +1247,7 @@ dependencies = [
|
||||
"axum-extra",
|
||||
"base64 0.13.1",
|
||||
"call",
|
||||
"clap 3.2.25",
|
||||
"clap",
|
||||
"client",
|
||||
"collections",
|
||||
"ctor",
|
||||
@@ -1426,7 +1419,6 @@ name = "copilot_button"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"context_menu",
|
||||
"copilot",
|
||||
"editor",
|
||||
@@ -1797,7 +1789,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1814,7 +1806,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2211,6 +2203,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -2438,6 +2440,7 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"regex",
|
||||
"rope",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -2587,7 +2590,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3512,6 +3515,29 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
"env_logger 0.9.3",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -3756,26 +3782,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsp_log"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach"
|
||||
version = "0.3.2"
|
||||
@@ -4348,7 +4354,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4793,6 +4799,16 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "0.1.5"
|
||||
@@ -4828,9 +4844,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.56"
|
||||
version = "1.0.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -5107,9 +5123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.27"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -6053,7 +6069,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6096,7 +6112,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6128,7 +6144,6 @@ name = "settings"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -6137,6 +6152,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -6583,12 +6599,6 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@@ -6660,9 +6670,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.15"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6848,15 +6858,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
@@ -6930,7 +6931,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6960,6 +6961,21 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.0",
|
||||
"bstr",
|
||||
"fancy-regex",
|
||||
"lazy_static",
|
||||
"parking_lot 0.12.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
@@ -7088,7 +7104,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7276,7 +7292,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7343,8 +7359,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.9"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
|
||||
version = "0.20.10"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=49226023693107fba9a1191136a4f47f38cdca73#49226023693107fba9a1191136a4f47f38cdca73"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -7381,8 +7397,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-elixir"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -7407,6 +7423,15 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-heex"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/phoenixframework/tree-sitter-heex?rev=2e1348c3cf2c9323e87c2744796cf3f3868aa82a#2e1348c3cf2c9323e87c2744796cf3f3868aa82a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-html"
|
||||
version = "0.19.0"
|
||||
@@ -7544,7 +7569,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=f545a41f57502e1b5ddf2a6668896c1b0620f930#f545a41f57502e1b5ddf2a6668896c1b0620f930"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -7778,6 +7803,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
@@ -7837,12 +7863,6 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@@ -7854,7 +7874,6 @@ name = "vim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compat",
|
||||
"async-trait",
|
||||
"collections",
|
||||
@@ -8006,7 +8025,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -8040,7 +8059,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -8682,7 +8701,6 @@ name = "workspace"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-recursion 1.0.4",
|
||||
"bincode",
|
||||
"call",
|
||||
@@ -8777,12 +8795,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.89.0"
|
||||
version = "0.91.3"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
@@ -8822,11 +8839,11 @@ dependencies = [
|
||||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
"language_tools",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"lsp_log",
|
||||
"node_runtime",
|
||||
"num_cpus",
|
||||
"outline",
|
||||
@@ -8868,6 +8885,7 @@ dependencies = [
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-heex",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
@@ -8907,7 +8925,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assets",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
"crates/call",
|
||||
@@ -33,10 +32,10 @@ members = [
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/lsp_log",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/node_runtime",
|
||||
@@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
rand = { version = "0.8.5" }
|
||||
regex = { version = "1.5" }
|
||||
rust-embed = { version = "6.3", features = ["include-exclude"] }
|
||||
schemars = { version = "0.8" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -98,10 +98,11 @@ tempdir = { version = "0.3.7" }
|
||||
thiserror = { version = "1.0.29" }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
toml = { version = "0.5" }
|
||||
tree-sitter = "0.20"
|
||||
unindent = { version = "0.1.7" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
@@ -116,3 +117,4 @@ split-debuginfo = "unpacked"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
lto = "thin"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.65-bullseye as builder
|
||||
FROM rust:1.70-bullseye as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
4
assets/icons/robot_14.svg
Normal file
4
assets/icons/robot_14.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
|
||||
<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
|
||||
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.01077 0.000234794C2.69085 0.000234794 0.000639074 2.18612 0.000639074 4.88385C0.000639074 6.0491 0.501914 7.11387 1.33823 7.95254C1.04475 9.13517 0.0640321 10.1894 0.0522927 10.2011C-0.00053487 10.2539 -0.0153266 10.3356 0.0170743 10.4061C0.0464229 10.4763 0.111459 10.5185 0.187766 10.5185C1.74324 10.5185 2.89019 9.77286 3.4889 9.31197C4.25431 9.60052 5.10894 9.76722 6.01053 9.76722C9.33045 9.76722 12 7.58063 12 4.88361C12 2.18659 9.33045 0 6.01053 0L6.01077 0.000234794Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 609 B |
@@ -16,6 +16,12 @@
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-g": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
|
||||
@@ -185,13 +185,8 @@
|
||||
],
|
||||
"alt-\\": "copilot::Suggest",
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == zmd",
|
||||
"bindings": {
|
||||
"cmd-enter": "ai::Assist"
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -201,6 +196,13 @@
|
||||
"cmd-alt-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
@@ -250,12 +252,24 @@
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-k ctrl-cmd-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
@@ -504,7 +518,7 @@
|
||||
"terminal::SendText",
|
||||
"\u0001"
|
||||
],
|
||||
// Terminal.app compatability
|
||||
// Terminal.app compatibility
|
||||
"alt-left": [
|
||||
"terminal::SendText",
|
||||
"\u001bb"
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-g": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
|
||||
@@ -81,6 +81,14 @@
|
||||
// Default width of the project panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"assistant": {
|
||||
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
|
||||
"dock": "right",
|
||||
// Default width when the assistant is docked to the left or right.
|
||||
"default_width": 450,
|
||||
// Default height when the assistant is docked to the bottom.
|
||||
"default_height": 320
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
@@ -100,6 +108,8 @@
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
"ensure_final_newline_on_save": true,
|
||||
@@ -108,9 +118,9 @@
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// "formatter": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "formatter": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
@@ -245,7 +255,7 @@
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
// environment. Use `:` to separate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
},
|
||||
|
||||
5
assets/settings/initial_local_settings.json
Normal file
5
assets/settings/initial_local_settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
|
||||
{}
|
||||
@@ -9,17 +9,26 @@ path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets"}
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = "0.4"
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tiktoken-rs = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::AsyncBufReadExt;
|
||||
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
|
||||
use gpui::executor::Background;
|
||||
use gpui::{actions, AppContext, Task, ViewContext};
|
||||
use isahc::prelude::*;
|
||||
use isahc::{http::StatusCode, Request};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::rc::Rc;
|
||||
use std::{io, sync::Arc};
|
||||
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
|
||||
actions!(ai, [Assist]);
|
||||
pub use assistant::AssistantPanel;
|
||||
use gpui::AppContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Serialize)]
|
||||
@@ -38,7 +26,7 @@ struct ResponseMessage {
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Role {
|
||||
User,
|
||||
@@ -46,6 +34,26 @@ enum Role {
|
||||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "User"),
|
||||
Role::Assistant => write!(f, "Assistant"),
|
||||
Role::System => write!(f, "System"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
@@ -86,228 +94,5 @@ struct OpenAIChoice {
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
|
||||
return;
|
||||
}
|
||||
|
||||
let assistant = Rc::new(Assistant::default());
|
||||
cx.add_action({
|
||||
let assistant = assistant.clone();
|
||||
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
|
||||
assistant.assist(editor, cx).log_err();
|
||||
}
|
||||
});
|
||||
cx.capture_action({
|
||||
let assistant = assistant.clone();
|
||||
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
|
||||
if !assistant.cancel_last_assist(cx.view_id()) {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Assistant(RefCell<AssistantState>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct AssistantState {
|
||||
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
|
||||
next_completion_id: CompletionId,
|
||||
}
|
||||
|
||||
impl Assistant {
|
||||
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
|
||||
let api_key = std::env::var("OPENAI_API_KEY")?;
|
||||
|
||||
let selections = editor.selections.all(cx);
|
||||
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
|
||||
// Insert markers around selected text as described in the system prompt above.
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let mut user_message = String::new();
|
||||
let mut user_message_suffix = String::new();
|
||||
let mut buffer_offset = 0;
|
||||
for selection in selections {
|
||||
if !selection.is_empty() {
|
||||
if user_message_suffix.is_empty() {
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
user_message_suffix.push_str("[Selected excerpt from above]\n");
|
||||
user_message_suffix
|
||||
.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
|
||||
user_message.push_str("[SELECTION_START]");
|
||||
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
buffer_offset = selection.end;
|
||||
user_message.push_str("[SELECTION_END]");
|
||||
}
|
||||
if buffer_offset < snapshot.len() {
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
|
||||
}
|
||||
user_message.push_str(&user_message_suffix);
|
||||
|
||||
// Ensure the document ends with 4 trailing newlines.
|
||||
let trailing_newline_count = snapshot
|
||||
.reversed_chars_at(snapshot.len())
|
||||
.take_while(|c| *c == '\n')
|
||||
.take(4);
|
||||
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
|
||||
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
|
||||
|
||||
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
|
||||
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
|
||||
|
||||
(user_message, insertion_site)
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
let buffer = editor.buffer().clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let editor_id = cx.view_id();
|
||||
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
|
||||
let assist_task = cx.spawn(|_, mut cx| {
|
||||
async move {
|
||||
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
|
||||
let content = Assets::get("contexts/system.zmd").unwrap();
|
||||
let mut system_message = std::str::from_utf8(content.data.as_ref())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if let Ok(custom_system_message_path) =
|
||||
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
|
||||
{
|
||||
system_message.push_str(
|
||||
"\n\nAlso consider the following user-defined system prompt:\n\n",
|
||||
);
|
||||
// TODO: Replace this with our file system trait object.
|
||||
system_message.push_str(
|
||||
&cx.background()
|
||||
.spawn(async move { fs::read_to_string(custom_system_message_path) })
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let stream = stream_completion(
|
||||
api_key,
|
||||
executor,
|
||||
OpenAIRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![
|
||||
RequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message.to_string(),
|
||||
},
|
||||
RequestMessage {
|
||||
role: Role::User,
|
||||
content: user_message,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
},
|
||||
);
|
||||
|
||||
let mut messages = stream.await?;
|
||||
while let Some(message) = messages.next().await {
|
||||
let mut message = message?;
|
||||
if let Some(choice) = message.choices.pop() {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let text: Arc<str> = choice.delta.content?.into();
|
||||
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.unwrap()
|
||||
.retain(|(id, _)| *id != assist_id);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.entry(cx.view_id())
|
||||
.or_default()
|
||||
.push((assist_id, assist_task));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.and_then(|assists| assists.pop())
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
tx.unbounded_send(event).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
))
|
||||
}
|
||||
assistant::init(cx);
|
||||
}
|
||||
|
||||
1525
crates/ai/src/assistant.rs
Normal file
1525
crates/ai/src/assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
40
crates/ai/src/assistant_settings.rs
Normal file
40
crates/ai/src/assistant_settings.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assets.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow.workspace = true
|
||||
rust-embed = { version = "6.3", features = ["include-exclude"] }
|
||||
@@ -1,29 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["install", "--no-save"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"failed to install theme dependencies {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["run", "build"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"build script failed {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=../../styles/src");
|
||||
}
|
||||
@@ -159,10 +159,7 @@ impl Bundle {
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath {
|
||||
executable: excutable,
|
||||
..
|
||||
} => excutable,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -776,15 +776,6 @@ impl Client {
|
||||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
cx.read(|cx| {
|
||||
self.telemetry().report_mixpanel_event(
|
||||
"read credentials from keychain",
|
||||
Default::default(),
|
||||
*settings::get::<TelemetrySettings>(cx),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if credentials.is_none() {
|
||||
let mut status_rx = self.status();
|
||||
@@ -1072,11 +1063,8 @@ impl Client {
|
||||
) -> Task<Result<Credentials>> {
|
||||
let platform = cx.platform();
|
||||
let executor = cx.background();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let http = self.http.clone();
|
||||
|
||||
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
|
||||
|
||||
executor.clone().spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
@@ -1159,12 +1147,6 @@ impl Client {
|
||||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_mixpanel_event(
|
||||
"authenticate with browser",
|
||||
Default::default(),
|
||||
telemetry_settings,
|
||||
);
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
serde_json::{self, value::Map, Value},
|
||||
AppContext, Task,
|
||||
};
|
||||
use gpui::{executor::Background, serde_json, AppContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
mem,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Telemetry {
|
||||
@@ -37,23 +25,15 @@ struct TelemetryState {
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
mixpanel_events_queue: Vec<MixpanelEvent>,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
next_mixpanel_event_id: usize,
|
||||
flush_mixpanel_events_task: Option<Task<()>>,
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
|
||||
lazy_static! {
|
||||
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
|
||||
static ref CLICKHOUSE_EVENTS_URL: String =
|
||||
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
|
||||
}
|
||||
@@ -72,7 +52,6 @@ struct ClickhouseEventRequestBody {
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventWrapper {
|
||||
time: u128,
|
||||
signed_in: bool,
|
||||
#[serde(flatten)]
|
||||
event: ClickhouseEvent,
|
||||
@@ -95,47 +74,6 @@ pub enum ClickhouseEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEvent {
|
||||
event: String,
|
||||
properties: MixpanelEventProperties,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEventProperties {
|
||||
// Mixpanel required fields
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
token: &'static str,
|
||||
time: u128,
|
||||
#[serde(rename = "distinct_id")]
|
||||
installation_id: Option<Arc<str>>,
|
||||
#[serde(rename = "$insert_id")]
|
||||
insert_id: usize,
|
||||
// Custom fields
|
||||
#[serde(skip_serializing_if = "Option::is_none", flatten)]
|
||||
event_properties: Option<Map<String, Value>>,
|
||||
#[serde(rename = "OS Name")]
|
||||
os_name: &'static str,
|
||||
#[serde(rename = "OS Version")]
|
||||
os_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Release Channel")]
|
||||
release_channel: Option<&'static str>,
|
||||
#[serde(rename = "App Version")]
|
||||
app_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Signed In")]
|
||||
signed_in: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MixpanelEngageRequest {
|
||||
#[serde(rename = "$token")]
|
||||
token: &'static str,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
installation_id: Arc<str>,
|
||||
#[serde(rename = "$set")]
|
||||
set: Value,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_QUEUE_LEN: usize = 1;
|
||||
|
||||
@@ -168,29 +106,13 @@ impl Telemetry {
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
mixpanel_events_queue: Default::default(),
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_mixpanel_events_task: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
next_mixpanel_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
});
|
||||
|
||||
if MIXPANEL_TOKEN.is_some() {
|
||||
this.executor
|
||||
.spawn({
|
||||
let this = this.clone();
|
||||
async move {
|
||||
if let Some(tempfile) = NamedTempFile::new().log_err() {
|
||||
this.state.lock().log_file = Some(tempfile);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
@@ -218,20 +140,9 @@ impl Telemetry {
|
||||
let mut state = this.state.lock();
|
||||
state.installation_id = Some(installation_id.clone());
|
||||
|
||||
for event in &mut state.mixpanel_events_queue {
|
||||
event
|
||||
.properties
|
||||
.installation_id
|
||||
.get_or_insert_with(|| installation_id.clone());
|
||||
}
|
||||
|
||||
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_mixpanel_events {
|
||||
this.flush_mixpanel_events();
|
||||
}
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
this.flush_clickhouse_events();
|
||||
@@ -256,37 +167,11 @@ impl Telemetry {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let installation_id = state.installation_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
|
||||
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
installation_id,
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
"App": true
|
||||
}),
|
||||
}])?;
|
||||
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_clickhouse_event(
|
||||
@@ -300,17 +185,12 @@ impl Telemetry {
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let signed_in = state.metrics_id.is_some();
|
||||
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
signed_in,
|
||||
event,
|
||||
});
|
||||
state
|
||||
.clickhouse_events_queue
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
@@ -324,55 +204,6 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_mixpanel_event(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = MixpanelEvent {
|
||||
event: kind.into(),
|
||||
properties: MixpanelEventProperties {
|
||||
token: "",
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
installation_id: state.installation_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_mixpanel_event_id),
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
release_channel: state.release_channel,
|
||||
app_version: state.app_version.clone(),
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
},
|
||||
};
|
||||
state.mixpanel_events_queue.push(event);
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_mixpanel_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush_mixpanel_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
@@ -385,44 +216,6 @@ impl Telemetry {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush_mixpanel_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.mixpanel_events_queue);
|
||||
state.flush_mixpanel_events_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
|
||||
event.properties.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &events)?;
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
|
||||
@@ -66,6 +66,7 @@ impl<'a> AddAssign<&'a Local> for Local {
|
||||
}
|
||||
}
|
||||
|
||||
/// A vector clock
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.12.4"
|
||||
version = "0.14.2"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -104,6 +104,8 @@ spec:
|
||||
key: secret
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
value: "1"
|
||||
- name: RUST_LOG
|
||||
value: ${RUST_LOG}
|
||||
- name: LOG_JSON
|
||||
|
||||
@@ -76,6 +76,7 @@ CREATE TABLE "worktree_entries" (
|
||||
"is_symlink" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"git_status" INTEGER,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -96,22 +97,16 @@ CREATE TABLE "worktree_repositories" (
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
"path" VARCHAR NOT NULL,
|
||||
"content" TEXT,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
|
||||
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
|
||||
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
|
||||
CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "worktree_entries"
|
||||
ADD "git_status" INT8;
|
||||
@@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
|
||||
mod worktree_entry;
|
||||
mod worktree_repository;
|
||||
mod worktree_repository_statuses;
|
||||
mod worktree_settings_file;
|
||||
|
||||
use crate::executor::Executor;
|
||||
use crate::{Error, Result};
|
||||
@@ -1494,6 +1495,7 @@ impl Database {
|
||||
updated_repositories: Default::default(),
|
||||
removed_repositories: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
};
|
||||
@@ -1537,6 +1539,7 @@ impl Database {
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1571,54 +1574,6 @@ impl Database {
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Status Entries
|
||||
for repository in worktree.updated_repositories.iter_mut() {
|
||||
let repository_status_entry_filter =
|
||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_repository_statuses::Column::ScanId
|
||||
.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository_statuses::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repository_statuses =
|
||||
worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id),
|
||||
)
|
||||
.add(repository_status_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_repository_statuses.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if db_status_entry.is_deleted {
|
||||
repository
|
||||
.removed_repo_paths
|
||||
.push(db_status_entry.repo_path);
|
||||
} else {
|
||||
repository.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1638,6 +1593,25 @@ impl Database {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) = worktrees
|
||||
.iter_mut()
|
||||
.find(|w| w.id == db_settings_file.worktree_id as u64)
|
||||
{
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut collaborators = project
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(&*tx)
|
||||
@@ -2375,6 +2349,7 @@ impl Database {
|
||||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||
is_symlink: ActiveValue::set(entry.is_symlink),
|
||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
}
|
||||
@@ -2393,6 +2368,7 @@ impl Database {
|
||||
worktree_entry::Column::MtimeNanos,
|
||||
worktree_entry::Column::IsSymlink,
|
||||
worktree_entry::Column::IsIgnored,
|
||||
worktree_entry::Column::GitStatus,
|
||||
worktree_entry::Column::ScanId,
|
||||
])
|
||||
.to_owned(),
|
||||
@@ -2446,68 +2422,6 @@ impl Database {
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
for repository in update.updated_repositories.iter() {
|
||||
if !repository.updated_statuses.is_empty() {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository_statuses::Column::ProjectId,
|
||||
worktree_repository_statuses::Column::WorktreeId,
|
||||
worktree_repository_statuses::Column::WorkDirectoryId,
|
||||
worktree_repository_statuses::Column::RepoPath,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
worktree_repository_statuses::Column::IsDeleted,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !repository.removed_repo_paths.is_empty() {
|
||||
worktree_repository_statuses::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree_id),
|
||||
)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id as i64),
|
||||
)
|
||||
.and(worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
repository.removed_repo_paths.iter().map(String::as_str),
|
||||
)),
|
||||
)
|
||||
.set(worktree_repository_statuses::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !update.removed_repositories.is_empty() {
|
||||
@@ -2637,6 +2551,58 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_worktree_settings(
|
||||
&self,
|
||||
update: &proto::UpdateWorktreeSettings,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.host_connection()? != connection {
|
||||
return Err(anyhow!("can't update a project hosted by someone else"))?;
|
||||
}
|
||||
|
||||
if let Some(content) = &update.content {
|
||||
worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
|
||||
project_id: ActiveValue::Set(project_id),
|
||||
worktree_id: ActiveValue::Set(update.worktree_id as i64),
|
||||
path: ActiveValue::Set(update.path.clone()),
|
||||
content: ActiveValue::Set(content.clone()),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_settings_file::Column::ProjectId,
|
||||
worktree_settings_file::Column::WorktreeId,
|
||||
worktree_settings_file::Column::Path,
|
||||
])
|
||||
.update_column(worktree_settings_file::Column::Content)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
|
||||
project_id: ActiveValue::Set(project_id),
|
||||
worktree_id: ActiveValue::Set(update.worktree_id as i64),
|
||||
path: ActiveValue::Set(update.path.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn join_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
@@ -2707,6 +2673,7 @@ impl Database {
|
||||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
},
|
||||
@@ -2738,6 +2705,7 @@ impl Database {
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2763,41 +2731,12 @@ impl Database {
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut db_status_entries = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_status_entries.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
|
||||
{
|
||||
if let Some(repository_entry) = worktree
|
||||
.repository_entries
|
||||
.get_mut(&(db_status_entry.work_directory_id as u64))
|
||||
{
|
||||
repository_entry.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
@@ -2819,6 +2758,25 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree settings files
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
|
||||
{
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
@@ -3482,6 +3440,7 @@ pub struct RejoinedWorktree {
|
||||
pub updated_repositories: Vec<proto::RepositoryEntry>,
|
||||
pub removed_repositories: Vec<u64>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub settings_files: Vec<WorktreeSettingsFile>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
}
|
||||
@@ -3537,10 +3496,17 @@ pub struct Worktree {
|
||||
pub entries: Vec<proto::Entry>,
|
||||
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub settings_files: Vec<WorktreeSettingsFile>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorktreeSettingsFile {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub use test::*;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct Model {
|
||||
pub inode: i64,
|
||||
pub mtime_seconds: i64,
|
||||
pub mtime_nanos: i32,
|
||||
pub git_status: Option<i64>,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
pub is_deleted: bool,
|
||||
|
||||
19
crates/collab/src/db/worktree_settings_file.rs
Normal file
19
crates/collab/src/db/worktree_settings_file.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_settings_files")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -200,6 +200,7 @@ impl Server {
|
||||
.add_message_handler(start_language_server)
|
||||
.add_message_handler(update_language_server)
|
||||
.add_message_handler(update_diagnostic_summary)
|
||||
.add_message_handler(update_worktree_settings)
|
||||
.add_request_handler(forward_project_request::<proto::GetHover>)
|
||||
.add_request_handler(forward_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
|
||||
@@ -1088,6 +1089,18 @@ async fn rejoin_room(
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
for settings_file in worktree.settings_files {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateWorktreeSettings {
|
||||
project_id: project.id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for language_server in &project.language_servers {
|
||||
@@ -1410,6 +1423,18 @@ async fn join_project(
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
for settings_file in worktree.settings_files {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateWorktreeSettings {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for language_server in &project.language_servers {
|
||||
@@ -1525,6 +1550,29 @@ async fn update_diagnostic_summary(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree_settings(
|
||||
message: proto::UpdateWorktreeSettings,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let guest_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.update_worktree_settings(&message, session.connection_id)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, message.clone())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
|
||||
@@ -39,7 +39,12 @@ use std::{
|
||||
},
|
||||
};
|
||||
use unindent::Unindent as _;
|
||||
use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
item::{test::TestItem, ItemHandle as _},
|
||||
shared_screen::SharedScreen,
|
||||
SplitDirection, Workspace,
|
||||
};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -2415,14 +2420,10 @@ async fn test_git_diff_base_change(
|
||||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
let buffer_local_a = project_local
|
||||
@@ -2464,14 +2465,10 @@ async fn test_git_diff_base_change(
|
||||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
@@ -2513,14 +2510,10 @@ async fn test_git_diff_base_change(
|
||||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), diff_base.clone())],
|
||||
);
|
||||
|
||||
// Create the buffer
|
||||
let buffer_local_b = project_local
|
||||
@@ -2562,14 +2555,10 @@ async fn test_git_diff_base_change(
|
||||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_index_for_repo(
|
||||
Path::new("/dir/sub/.git"),
|
||||
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||
);
|
||||
|
||||
// Wait for buffer_local_b to receive it
|
||||
deterministic.run_until_parked();
|
||||
@@ -2646,8 +2635,7 @@ async fn test_git_branch_name(
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
|
||||
.await;
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
|
||||
|
||||
// Wait for it to catch up to the new branch
|
||||
deterministic.run_until_parked();
|
||||
@@ -2673,8 +2661,7 @@ async fn test_git_branch_name(
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
|
||||
.await;
|
||||
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
@@ -2726,17 +2713,13 @@ async fn test_git_status_sync(
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
(&Path::new(B_TXT), GitFileStatus::Added),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
client_a.fs.as_fake().set_status_for_repo_via_git_operation(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Added),
|
||||
(&Path::new(B_TXT), GitFileStatus::Added),
|
||||
],
|
||||
);
|
||||
|
||||
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
@@ -2763,8 +2746,7 @@ async fn test_git_status_sync(
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_entry = snapshot.root_git_entry().unwrap();
|
||||
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
|
||||
assert_eq!(snapshot.status_for_file(file), status);
|
||||
}
|
||||
|
||||
// Smoke test status reading
|
||||
@@ -2780,14 +2762,13 @@ async fn test_git_status_sync(
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_status_for_repo(
|
||||
.set_status_for_repo_via_working_copy_change(
|
||||
Path::new("/dir/.git"),
|
||||
&[
|
||||
(&Path::new(A_TXT), GitFileStatus::Modified),
|
||||
(&Path::new(B_TXT), GitFileStatus::Modified),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
);
|
||||
|
||||
// Wait for buffer_local_a to receive it
|
||||
deterministic.run_until_parked();
|
||||
@@ -3114,6 +3095,135 @@ async fn test_fs_operations(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_local_settings(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).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);
|
||||
|
||||
// As client A, open a project that contains some local settings files
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 2 }"#
|
||||
},
|
||||
"a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 8 }"#
|
||||
},
|
||||
"a.txt": "a-contents",
|
||||
},
|
||||
"b": {
|
||||
"b.txt": "b-contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// As client B, join that project and observe the local settings.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client A, update a settings file. As Client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client A, create and remove some settings files. As client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.create_dir("/dir/b/.zed".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client B, disconnect.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b.peer_id().unwrap());
|
||||
|
||||
// As client A, change and remove settings files while client B is disconnected.
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// As client B, reconnect and see the changed settings.
|
||||
server.allow_connections();
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_conflict_after_save(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -6742,12 +6852,43 @@ async fn test_basic_following(
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates an external window again, and the previously-opened screen-sharing item
|
||||
// gets activated.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
let panel = cx_b.add_view(workspace_b.window_id(), |_| {
|
||||
TestPanel::new(DockPosition::Left)
|
||||
});
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.add_panel(panel, cx);
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.id()),
|
||||
shared_screen.id()
|
||||
);
|
||||
|
||||
// Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().id(),
|
||||
multibuffer_editor_a.id()
|
||||
)
|
||||
});
|
||||
|
||||
// Client B activates an item that doesn't implement following,
|
||||
// so the previously-opened screen-sharing item gets activated.
|
||||
let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new());
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
})
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
|
||||
@@ -422,7 +422,7 @@ async fn apply_client_operation(
|
||||
);
|
||||
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
if !client.fs.paths().contains(&new_root_path) {
|
||||
if !client.fs.paths(false).contains(&new_root_path) {
|
||||
client.fs.create_dir(&new_root_path).await.unwrap();
|
||||
}
|
||||
project
|
||||
@@ -628,12 +628,13 @@ async fn apply_client_operation(
|
||||
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
|
||||
let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
|
||||
let save = cx.background().spawn(async move {
|
||||
let (saved_version, _, _) = save
|
||||
.await
|
||||
let save = project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
|
||||
let save = cx.spawn(|cx| async move {
|
||||
save.await
|
||||
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
|
||||
assert!(saved_version.observed_all(&requested_version));
|
||||
assert!(buffer
|
||||
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
|
||||
.observed_all(&requested_version));
|
||||
anyhow::Ok(())
|
||||
});
|
||||
if detach {
|
||||
@@ -743,7 +744,7 @@ async fn apply_client_operation(
|
||||
} => {
|
||||
if !client
|
||||
.fs
|
||||
.directories()
|
||||
.directories(false)
|
||||
.contains(&path.parent().unwrap().to_owned())
|
||||
{
|
||||
return Err(TestError::Inapplicable);
|
||||
@@ -770,10 +771,16 @@ async fn apply_client_operation(
|
||||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
for (path, _) in contents.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
@@ -789,13 +796,13 @@ async fn apply_client_operation(
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents);
|
||||
}
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
@@ -810,15 +817,21 @@ async fn apply_client_operation(
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch);
|
||||
}
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
git_operation,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
if !client.fs.directories(false).contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
for (path, _) in statuses.iter() {
|
||||
if !client.fs.files().contains(&repo_path.join(path)) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git statuses for repo {:?}: {:?}",
|
||||
@@ -838,10 +851,16 @@ async fn apply_client_operation(
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
|
||||
.await;
|
||||
if git_operation {
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
|
||||
} else {
|
||||
client.fs.set_status_for_repo_via_working_copy_change(
|
||||
&dot_git_dir,
|
||||
statuses.as_slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -913,9 +932,10 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} and project {:?}",
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
id,
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
@@ -1230,6 +1250,7 @@ enum GitOperation {
|
||||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
git_operation: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1575,7 +1596,7 @@ impl TestPlan {
|
||||
.choose(&mut self.rng)
|
||||
.cloned() else { continue };
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let mut paths = client.fs.paths();
|
||||
let mut paths = client.fs.paths(false);
|
||||
paths.remove(0);
|
||||
let new_root_path = if paths.is_empty() || self.rng.gen() {
|
||||
Path::new("/").join(&self.next_root_dir_name(user_id))
|
||||
@@ -1755,7 +1776,7 @@ impl TestPlan {
|
||||
let is_dir = self.rng.gen::<bool>();
|
||||
let content;
|
||||
let mut path;
|
||||
let dir_paths = client.fs.directories();
|
||||
let dir_paths = client.fs.directories(false);
|
||||
|
||||
if is_dir {
|
||||
content = String::new();
|
||||
@@ -1809,7 +1830,7 @@ impl TestPlan {
|
||||
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.directories(false)
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
@@ -1855,9 +1876,12 @@ impl TestPlan {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let git_operation = self.rng.gen::<bool>();
|
||||
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
git_operation,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -472,7 +472,7 @@ impl CollabTitlebarItem {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
//TODO: Ensure this button has consistent width for both text variations
|
||||
let style = titlebar.share_button.style_for(state, false);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
|
||||
@@ -4,7 +4,7 @@ mod sign_in;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
|
||||
@@ -127,7 +127,7 @@ impl CopilotServer {
|
||||
struct RunningCopilotServer {
|
||||
lsp: Arc<LanguageServer>,
|
||||
sign_in_status: SignInStatus,
|
||||
registered_buffers: HashMap<u64, RegisteredBuffer>,
|
||||
registered_buffers: HashMap<usize, RegisteredBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -163,7 +163,6 @@ impl Status {
|
||||
}
|
||||
|
||||
struct RegisteredBuffer {
|
||||
id: u64,
|
||||
uri: lsp::Url,
|
||||
language_id: String,
|
||||
snapshot: BufferSnapshot,
|
||||
@@ -178,13 +177,13 @@ impl RegisteredBuffer {
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Copilot>,
|
||||
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
|
||||
let id = self.id;
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
if buffer.read(cx).version() == self.snapshot.version {
|
||||
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
|
||||
} else {
|
||||
let buffer = buffer.downgrade();
|
||||
let id = buffer.id();
|
||||
let prev_pending_change =
|
||||
mem::replace(&mut self.pending_buffer_change, Task::ready(None));
|
||||
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
|
||||
@@ -268,7 +267,7 @@ pub struct Copilot {
|
||||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<NodeRuntime>,
|
||||
server: CopilotServer,
|
||||
buffers: HashMap<u64, WeakModelHandle<Buffer>>,
|
||||
buffers: HashSet<WeakModelHandle<Buffer>>,
|
||||
}
|
||||
|
||||
impl Entity for Copilot {
|
||||
@@ -318,7 +317,7 @@ impl Copilot {
|
||||
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
if all_language_settings(cx).copilot_enabled(None, None) {
|
||||
if all_language_settings(None, cx).copilot_enabled(None, None) {
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let start_task = cx
|
||||
.spawn({
|
||||
@@ -375,7 +374,7 @@ impl Copilot {
|
||||
server
|
||||
.on_notification::<LogMessage, _>(|params, _cx| {
|
||||
match params.level {
|
||||
// Copilot is pretty agressive about logging
|
||||
// Copilot is pretty aggressive about logging
|
||||
0 => debug!("copilot: {}", params.message),
|
||||
1 => debug!("copilot: {}", params.message),
|
||||
_ => error!("copilot: {}", params.message),
|
||||
@@ -559,8 +558,8 @@ impl Copilot {
|
||||
}
|
||||
|
||||
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
self.buffers.insert(buffer_id, buffer.downgrade());
|
||||
let weak_buffer = buffer.downgrade();
|
||||
self.buffers.insert(weak_buffer.clone());
|
||||
|
||||
if let CopilotServer::Running(RunningCopilotServer {
|
||||
lsp: server,
|
||||
@@ -573,8 +572,7 @@ impl Copilot {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
registered_buffers.entry(buffer_id).or_insert_with(|| {
|
||||
registered_buffers.entry(buffer.id()).or_insert_with(|| {
|
||||
let uri: lsp::Url = uri_for_buffer(buffer, cx);
|
||||
let language_id = id_for_language(buffer.read(cx).language());
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
@@ -592,7 +590,6 @@ impl Copilot {
|
||||
.log_err();
|
||||
|
||||
RegisteredBuffer {
|
||||
id: buffer_id,
|
||||
uri,
|
||||
language_id,
|
||||
snapshot,
|
||||
@@ -603,8 +600,8 @@ impl Copilot {
|
||||
this.handle_buffer_event(buffer, event, cx).log_err();
|
||||
}),
|
||||
cx.observe_release(buffer, move |this, _buffer, _cx| {
|
||||
this.buffers.remove(&buffer_id);
|
||||
this.unregister_buffer(buffer_id);
|
||||
this.buffers.remove(&weak_buffer);
|
||||
this.unregister_buffer(&weak_buffer);
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -619,8 +616,7 @@ impl Copilot {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
|
||||
match event {
|
||||
language::Event::Edited => {
|
||||
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||
@@ -674,9 +670,9 @@ impl Copilot {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister_buffer(&mut self, buffer_id: u64) {
|
||||
fn unregister_buffer(&mut self, buffer: &WeakModelHandle<Buffer>) {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
|
||||
if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||
@@ -779,16 +775,12 @@ impl Copilot {
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let lsp = server.lsp.clone();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
|
||||
let snapshot = registered_buffer.report_changes(buffer, cx);
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()).as_deref(),
|
||||
cx,
|
||||
);
|
||||
let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
@@ -853,7 +845,7 @@ impl Copilot {
|
||||
lsp_status: request::SignInStatus,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
|
||||
self.buffers.retain(|buffer| buffer.is_upgradable(cx));
|
||||
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
match lsp_status {
|
||||
@@ -861,7 +853,7 @@ impl Copilot {
|
||||
| request::SignInStatus::MaybeOk { .. }
|
||||
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||
server.sign_in_status = SignInStatus::Authorized;
|
||||
for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
self.register_buffer(&buffer, cx);
|
||||
}
|
||||
@@ -869,14 +861,14 @@ impl Copilot {
|
||||
}
|
||||
request::SignInStatus::NotAuthorized { .. } => {
|
||||
server.sign_in_status = SignInStatus::Unauthorized;
|
||||
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(buffer_id);
|
||||
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
}
|
||||
request::SignInStatus::NotSignedIn => {
|
||||
server.sign_in_status = SignInStatus::SignedOut;
|
||||
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(buffer_id);
|
||||
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
|
||||
} else {
|
||||
format!("buffer://{}", buffer.read(cx).remote_id())
|
||||
.parse()
|
||||
.unwrap()
|
||||
format!("buffer://{}", buffer.id()).parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,6 +1165,10 @@ mod tests {
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl language::LocalFile for File {
|
||||
|
||||
@@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
copilot = { path = "../copilot" }
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
|
||||
@@ -9,7 +9,10 @@ use gpui::{
|
||||
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::language_settings::{self, all_language_settings, AllLanguageSettings};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
@@ -26,8 +29,8 @@ pub struct CopilotButton {
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<str>>,
|
||||
path: Option<Arc<Path>>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
@@ -41,7 +44,7 @@ impl View for CopilotButton {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let all_language_settings = &all_language_settings(cx);
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
@@ -165,7 +168,7 @@ impl CopilotButton {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
path: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
@@ -197,14 +200,13 @@ impl CopilotButton {
|
||||
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(language.as_ref()), cx)
|
||||
.show_copilot_suggestions;
|
||||
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language
|
||||
language.name()
|
||||
),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
));
|
||||
@@ -212,9 +214,9 @@ impl CopilotButton {
|
||||
|
||||
let settings = settings::get::<AllLanguageSettings>(cx);
|
||||
|
||||
if let Some(path) = self.path.as_ref() {
|
||||
let path_enabled = settings.copilot_enabled_for_path(path);
|
||||
let path = path.clone();
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
@@ -276,17 +278,15 @@ impl CopilotButton {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language_name = snapshot
|
||||
.language_at(suggestion_anchor)
|
||||
.map(|language| language.name());
|
||||
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(cx)
|
||||
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
|
||||
all_language_settings(self.file.as_ref(), cx)
|
||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||
);
|
||||
self.language = language_name;
|
||||
self.path = path.cloned();
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
@@ -315,9 +315,7 @@ async fn configure_disabled_globs(
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content(&assets::Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
@@ -363,17 +361,18 @@ async fn configure_disabled_globs(
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language)
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
|
||||
@@ -430,7 +430,7 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, cx);
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
let block_ids = editor.insert_blocks(
|
||||
blocks_to_add.into_iter().map(|block| {
|
||||
let (excerpt_id, text_anchor) = block.position;
|
||||
@@ -442,6 +442,7 @@ impl ProjectDiagnosticsEditor {
|
||||
disposition: block.disposition,
|
||||
}
|
||||
}),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-rust = "0.20"
|
||||
tree-sitter-html = "0.19"
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
|
||||
@@ -272,12 +272,11 @@ impl DisplayMap {
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
|
||||
let language_name = buffer
|
||||
let language = buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).language())
|
||||
.map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
.and_then(|buffer| buffer.read(cx).language());
|
||||
language_settings(language.as_deref(), None, cx).tab_size
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -476,7 +475,7 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
|
||||
/// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn find_while<'a>(
|
||||
&'a self,
|
||||
@@ -487,7 +486,7 @@ impl DisplaySnapshot {
|
||||
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
|
||||
}
|
||||
|
||||
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
|
||||
/// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn reverse_find_while<'a>(
|
||||
&'a self,
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod items;
|
||||
mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod multi_buffer;
|
||||
pub mod multi_buffer;
|
||||
mod persistence;
|
||||
pub mod scroll;
|
||||
pub mod selections_collection;
|
||||
@@ -31,7 +31,9 @@ use copilot::Copilot;
|
||||
pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use editor_settings::EditorSettings;
|
||||
pub use element::*;
|
||||
pub use element::{
|
||||
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -44,9 +46,9 @@ use gpui::{
|
||||
impl_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json::{self, json},
|
||||
AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity,
|
||||
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
|
||||
Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -109,6 +111,12 @@ pub struct SelectNext {
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq, Default)]
|
||||
pub struct SelectPrevious {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SelectToBeginningOfLine {
|
||||
#[serde(default)]
|
||||
@@ -270,6 +278,7 @@ impl_actions!(
|
||||
editor,
|
||||
[
|
||||
SelectNext,
|
||||
SelectPrevious,
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
ToggleCodeActions,
|
||||
@@ -365,6 +374,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::add_selection_above);
|
||||
cx.add_action(Editor::add_selection_below);
|
||||
cx.add_action(Editor::select_next);
|
||||
cx.add_action(Editor::select_previous);
|
||||
cx.add_action(Editor::toggle_comments);
|
||||
cx.add_action(Editor::select_larger_syntax_node);
|
||||
cx.add_action(Editor::select_smaller_syntax_node);
|
||||
@@ -482,6 +492,7 @@ pub struct Editor {
|
||||
columnar_selection_tail: Option<Anchor>,
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
select_next_state: Option<SelectNextState>,
|
||||
select_prev_state: Option<SelectNextState>,
|
||||
selection_history: SelectionHistory,
|
||||
autoclose_regions: Vec<AutocloseRegion>,
|
||||
snippet_stack: InvalidationStack<SnippetState>,
|
||||
@@ -496,6 +507,7 @@ pub struct Editor {
|
||||
blink_manager: ModelHandle<BlinkManager>,
|
||||
show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -526,6 +538,7 @@ pub struct Editor {
|
||||
|
||||
pub struct EditorSnapshot {
|
||||
pub mode: EditorMode,
|
||||
pub show_gutter: bool,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
is_focused: bool,
|
||||
@@ -537,6 +550,7 @@ pub struct EditorSnapshot {
|
||||
struct SelectionHistoryEntry {
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
select_next_state: Option<SelectNextState>,
|
||||
select_prev_state: Option<SelectNextState>,
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
@@ -1284,6 +1298,7 @@ impl Editor {
|
||||
columnar_selection_tail: None,
|
||||
add_selections_state: None,
|
||||
select_next_state: None,
|
||||
select_prev_state: None,
|
||||
selection_history: Default::default(),
|
||||
autoclose_regions: Default::default(),
|
||||
snippet_stack: Default::default(),
|
||||
@@ -1297,6 +1312,7 @@ impl Editor {
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
mode,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
placeholder_text: None,
|
||||
highlighted_rows: None,
|
||||
background_highlights: Default::default(),
|
||||
@@ -1393,6 +1409,7 @@ impl Editor {
|
||||
pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
|
||||
EditorSnapshot {
|
||||
mode: self.mode,
|
||||
show_gutter: self.show_gutter,
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
|
||||
@@ -1505,6 +1522,7 @@ impl Editor {
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
self.add_selections_state = None;
|
||||
self.select_next_state = None;
|
||||
self.select_prev_state = None;
|
||||
self.select_larger_syntax_node_stack.clear();
|
||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
|
||||
self.snippet_stack
|
||||
@@ -2147,8 +2165,8 @@ impl Editor {
|
||||
self.transact(cx, |this, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
|
||||
let buffer = this.buffer.read(cx).snapshot(cx);
|
||||
let multi_buffer = this.buffer.read(cx);
|
||||
let buffer = multi_buffer.snapshot(cx);
|
||||
selections
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
@@ -2157,9 +2175,11 @@ impl Editor {
|
||||
indent.len = cmp::min(indent.len, start_point.column);
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
|
||||
let mut insert_extra_newline = false;
|
||||
if let Some(language) = buffer.language_scope_at(start) {
|
||||
let is_cursor = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||
&language_scope
|
||||
{
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
@@ -2172,25 +2192,71 @@ impl Editor {
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
let insert_extra_newline =
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer
|
||||
.contains_str_at(end + trailing_whitespace_len, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(
|
||||
end + trailing_whitespace_len,
|
||||
pair_end,
|
||||
)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = language.line_comment_prefix().filter(|_| {
|
||||
let is_comment_extension_enabled =
|
||||
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
|
||||
is_cursor && is_comment_extension_enabled
|
||||
});
|
||||
}
|
||||
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
|
||||
buffer
|
||||
.buffer_line_for_row(start_point.row)
|
||||
.is_some_and(|(snapshot, range)| {
|
||||
let mut index_of_first_non_whitespace = 0;
|
||||
let line_starts_with_comment = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| {
|
||||
let should_skip = c.is_whitespace();
|
||||
if should_skip {
|
||||
index_of_first_non_whitespace += 1;
|
||||
}
|
||||
should_skip
|
||||
})
|
||||
.take(delimiter.len())
|
||||
.eq(delimiter.chars());
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
index_of_first_non_whitespace + delimiter.len()
|
||||
<= start_point.column as usize;
|
||||
line_starts_with_comment
|
||||
&& cursor_is_placed_after_comment_marker
|
||||
})
|
||||
.then(|| delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
let mut new_text = String::with_capacity(1 + indent.len as usize);
|
||||
new_text.push('\n');
|
||||
let capacity_for_delimiter = comment_delimiter
|
||||
.as_deref()
|
||||
.map(str::len)
|
||||
.unwrap_or_default();
|
||||
let mut new_text =
|
||||
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
|
||||
new_text.push_str("\n");
|
||||
new_text.extend(indent.chars());
|
||||
if let Some(delimiter) = &comment_delimiter {
|
||||
new_text.push_str(&delimiter);
|
||||
}
|
||||
if insert_extra_newline {
|
||||
new_text = new_text.repeat(2);
|
||||
}
|
||||
@@ -2372,7 +2438,7 @@ impl Editor {
|
||||
old_selections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let anchor = snapshot.anchor_after(s.end);
|
||||
let anchor = snapshot.anchor_after(s.head());
|
||||
s.map(|_| anchor)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -2525,7 +2591,7 @@ impl Editor {
|
||||
.read(cx)
|
||||
.text_anchor_for_position(position.clone(), cx)?;
|
||||
|
||||
// OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances,
|
||||
// OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
|
||||
// hence we do LSP request & edit on host side only — add formats to host's history.
|
||||
let push_to_lsp_host_history = true;
|
||||
// If this is not the host, append its history with new edits.
|
||||
@@ -3207,12 +3273,10 @@ impl Editor {
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let path = snapshot.file_at(location).map(|file| file.path().as_ref());
|
||||
let language_name = snapshot
|
||||
.language_at(location)
|
||||
.map(|language| language.name());
|
||||
let settings = all_language_settings(cx);
|
||||
settings.copilot_enabled(language_name.as_deref(), path)
|
||||
let file = snapshot.file_at(location);
|
||||
let language = snapshot.language_at(location);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
|
||||
}
|
||||
|
||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
||||
@@ -3549,7 +3613,9 @@ impl Editor {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
let cursor = movement::right(map, selection.head());
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
selection.end = cursor;
|
||||
selection.reversed = true;
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -5213,6 +5279,101 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_selection_history();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let mut selections = self.selections.all::<usize>(cx);
|
||||
if let Some(mut select_prev_state) = self.select_prev_state.take() {
|
||||
let query = &select_prev_state.query;
|
||||
if !select_prev_state.done {
|
||||
let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
|
||||
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
|
||||
let mut next_selected_range = None;
|
||||
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
|
||||
let bytes_before_last_selection =
|
||||
buffer.reversed_bytes_in_range(0..last_selection.start);
|
||||
let bytes_after_first_selection =
|
||||
buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
|
||||
let query_matches = query
|
||||
.stream_find_iter(bytes_before_last_selection)
|
||||
.map(|result| (last_selection.start, result))
|
||||
.chain(
|
||||
query
|
||||
.stream_find_iter(bytes_after_first_selection)
|
||||
.map(|result| (buffer.len(), result)),
|
||||
);
|
||||
for (end_offset, query_match) in query_matches {
|
||||
let query_match = query_match.unwrap(); // can only fail due to I/O
|
||||
let offset_range =
|
||||
end_offset - query_match.end()..end_offset - query_match.start();
|
||||
let display_range = offset_range.start.to_display_point(&display_map)
|
||||
..offset_range.end.to_display_point(&display_map);
|
||||
|
||||
if !select_prev_state.wordwise
|
||||
|| (!movement::is_inside_word(&display_map, display_range.start)
|
||||
&& !movement::is_inside_word(&display_map, display_range.end))
|
||||
{
|
||||
next_selected_range = Some(offset_range);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
s.insert_range(next_selected_range);
|
||||
});
|
||||
} else {
|
||||
select_prev_state.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.select_prev_state = Some(select_prev_state);
|
||||
} else if selections.len() == 1 {
|
||||
let selection = selections.last_mut().unwrap();
|
||||
if selection.start == selection.end {
|
||||
let word_range = movement::surrounding_word(
|
||||
&display_map,
|
||||
selection.start.to_display_point(&display_map),
|
||||
);
|
||||
selection.start = word_range.start.to_offset(&display_map, Bias::Left);
|
||||
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
|
||||
selection.goal = SelectionGoal::None;
|
||||
selection.reversed = false;
|
||||
|
||||
let query = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
let select_state = SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
wordwise: true,
|
||||
done: false,
|
||||
};
|
||||
self.unfold_ranges([selection.start..selection.end], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
self.select_prev_state = Some(select_state);
|
||||
} else {
|
||||
let query = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_previous(action, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let mut selections = this.selections.all::<Point>(cx);
|
||||
@@ -5331,7 +5492,7 @@ impl Editor {
|
||||
let mut all_selection_lines_are_comments = true;
|
||||
|
||||
for row in start_row..=end_row {
|
||||
if snapshot.is_line_blank(row) {
|
||||
if snapshot.is_line_blank(row) && start_row < end_row {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5586,6 +5747,7 @@ impl Editor {
|
||||
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
@@ -5598,6 +5760,7 @@ impl Editor {
|
||||
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
@@ -6105,6 +6268,7 @@ impl Editor {
|
||||
}),
|
||||
disposition: BlockDisposition::Below,
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
)[0];
|
||||
this.pending_rename = Some(RenameState {
|
||||
@@ -6171,7 +6335,11 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<RenameState> {
|
||||
let rename = self.pending_rename.take()?;
|
||||
self.remove_blocks([rename.block_id].into_iter().collect(), cx);
|
||||
self.remove_blocks(
|
||||
[rename.block_id].into_iter().collect(),
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
self.clear_text_highlights::<Rename>(cx);
|
||||
self.show_local_selections = true;
|
||||
|
||||
@@ -6375,6 +6543,7 @@ impl Editor {
|
||||
self.selection_history.push(SelectionHistoryEntry {
|
||||
selections: self.selections.disjoint_anchors(),
|
||||
select_next_state: self.select_next_state.clone(),
|
||||
select_prev_state: self.select_prev_state.clone(),
|
||||
add_selections_state: self.add_selections_state.clone(),
|
||||
});
|
||||
}
|
||||
@@ -6556,29 +6725,43 @@ impl Editor {
|
||||
pub fn insert_blocks(
|
||||
&mut self,
|
||||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<BlockId> {
|
||||
let blocks = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
blocks: HashMap<BlockId, RenderBlock>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
||||
pub fn remove_blocks(
|
||||
&mut self,
|
||||
block_ids: HashSet<BlockId>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_blocks(block_ids, cx)
|
||||
});
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
|
||||
@@ -6654,6 +6837,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_gutter = show_gutter;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
@@ -6878,7 +7066,7 @@ impl Editor {
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
}
|
||||
multi_buffer::Event::LanguageChanged => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6919,7 +7107,7 @@ impl Editor {
|
||||
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in editor.selections.all::<usize>(cx) {
|
||||
for (buffer, mut range) in
|
||||
for (buffer, mut range, _) in
|
||||
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
@@ -7076,22 +7264,24 @@ impl Editor {
|
||||
};
|
||||
|
||||
// If None, we are in a file without an extension
|
||||
let file_extension = file_extension.or(self
|
||||
let file = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file())
|
||||
.and_then(|b| b.read(cx).file());
|
||||
let file_extension = file_extension.or(file
|
||||
.as_ref()
|
||||
.and_then(|file| Path::new(file.file_name(cx)).extension())
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = cx
|
||||
.global::<SettingsStore>()
|
||||
.untyped_user_settings()
|
||||
.raw_user_settings()
|
||||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true));
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
|
||||
let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
|
||||
let copilot_enabled_for_language = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
@@ -7099,15 +7289,6 @@ impl Editor {
|
||||
.show_copilot_suggestions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_mixpanel_event(
|
||||
match name {
|
||||
"open" => "open editor",
|
||||
"save" => "save editor",
|
||||
_ => name,
|
||||
},
|
||||
json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }),
|
||||
telemetry_settings,
|
||||
);
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
@@ -7268,6 +7449,7 @@ pub enum Event {
|
||||
},
|
||||
ScrollPositionChanged {
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
},
|
||||
Closed,
|
||||
}
|
||||
@@ -7801,13 +7983,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
inital_highlights: Vec<usize>,
|
||||
initial_highlights: Vec<usize>,
|
||||
message: &str,
|
||||
) -> (String, Vec<usize>) {
|
||||
let mut message_without_backticks = String::new();
|
||||
let mut prev_offset = 0;
|
||||
let mut inside_block = false;
|
||||
let mut highlights = inital_highlights;
|
||||
let mut highlights = initial_highlights;
|
||||
for (match_ix, (offset, _)) in message
|
||||
.match_indices('`')
|
||||
.chain([(message.len(), "")])
|
||||
|
||||
@@ -9,7 +9,8 @@ use gpui::{
|
||||
executor::Deterministic,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
serde_json, TestAppContext,
|
||||
serde_json::{self, json},
|
||||
TestAppContext,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
@@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
|
||||
|
||||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor;
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
|
||||
invalid_anchor.text_anchor.buffer_id = Some(999);
|
||||
let invalid_point = Point::new(9999, 0);
|
||||
editor.navigate(
|
||||
@@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||
cursor_anchor: invalid_anchor,
|
||||
cursor_position: invalid_point,
|
||||
scroll_anchor: ScrollAnchor {
|
||||
top_anchor: invalid_anchor,
|
||||
anchor: invalid_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
scroll_top_row: invalid_point.row,
|
||||
@@ -1718,6 +1719,56 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("//".into()),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
{
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
// Fooˇ
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
//ˇ
|
||||
"});
|
||||
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
|
||||
cx.set_state(indoc! {"
|
||||
ˇ// Foo
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ// Foo
|
||||
"});
|
||||
}
|
||||
// Ensure that comment continuations can be disabled.
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.defaults.extend_comment_on_newline = Some(false);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
// Fooˇ
|
||||
"});
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -2444,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||
height: 1,
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}],
|
||||
Some(Autoscroll::fit()),
|
||||
cx,
|
||||
);
|
||||
editor.change_selections(None, cx, |s| {
|
||||
@@ -3107,6 +3159,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
{
|
||||
// `Select previous` without a selection (selects wordwise)
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
{
|
||||
// `Select previous` with a selection
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -4270,7 +4373,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overriden tabsize is sent to language server
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
@@ -4384,7 +4487,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overriden tabsize is sent to language server
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
@@ -4725,7 +4828,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"overlapping aditional edit",
|
||||
"overlapping additional edit",
|
||||
),
|
||||
(
|
||||
indoc! {"
|
||||
@@ -4842,7 +4945,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("// ".into()),
|
||||
@@ -4850,77 +4953,95 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
let text = "
|
||||
// If multiple selections intersect a line, the line is only toggled once.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
//b();
|
||||
// c();
|
||||
// d();
|
||||
«//b();
|
||||
ˇ»// «c();
|
||||
//ˇ» d();
|
||||
}
|
||||
"
|
||||
.unindent();
|
||||
"});
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
|
||||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||||
|
||||
view.update(cx, |editor, cx| {
|
||||
// If multiple selections intersect a line, the line is only
|
||||
// toggled once.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3),
|
||||
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
|
||||
])
|
||||
});
|
||||
editor.toggle_comments(&ToggleComments::default(), cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
fn a() {
|
||||
b();
|
||||
c();
|
||||
d();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
«b();
|
||||
c();
|
||||
ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
// The comment prefix is inserted at the same column for every line
|
||||
// in a selection.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)])
|
||||
});
|
||||
editor.toggle_comments(&ToggleComments::default(), cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
fn a() {
|
||||
// b();
|
||||
// c();
|
||||
// d();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
// The comment prefix is inserted at the same column for every line in a
|
||||
// selection.
|
||||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||||
|
||||
// If a selection ends at the beginning of a line, that line is not toggled.
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)])
|
||||
});
|
||||
editor.toggle_comments(&ToggleComments::default(), cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"
|
||||
fn a() {
|
||||
// b();
|
||||
c();
|
||||
// d();
|
||||
}
|
||||
"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «b();
|
||||
// c();
|
||||
ˇ»// d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection ends at the beginning of a line, that line is not toggled.
|
||||
cx.set_selections_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
«// c();
|
||||
ˇ» // d();
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
«c();
|
||||
ˇ» // d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span a single line and is empty, the line is toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
//•ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span multiple lines, empty lines are not toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
«a();
|
||||
|
||||
c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «a();
|
||||
|
||||
// c();ˇ»
|
||||
}
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -5225,7 +5346,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||
Point::new(0, 1)..Point::new(0, 1),
|
||||
Point::new(1, 1)..Point::new(1, 1),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure the cursor's head is respected when deleting across an excerpt boundary.
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
|
||||
});
|
||||
view.backspace(&Default::default(), cx);
|
||||
assert_eq!(view.text(cx), "Xa\nbbb");
|
||||
assert_eq!(
|
||||
view.selections.ranges(cx),
|
||||
[Point::new(1, 0)..Point::new(1, 0)]
|
||||
);
|
||||
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
|
||||
});
|
||||
view.backspace(&Default::default(), cx);
|
||||
assert_eq!(view.text(cx), "X\nbb");
|
||||
assert_eq!(
|
||||
view.selections.ranges(cx),
|
||||
[Point::new(0, 1)..Point::new(0, 1)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5742,7 +5884,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
||||
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
|
||||
follower.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
offset: vec2f(0.0, 0.5),
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -91,7 +91,6 @@ impl SelectionLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditorElement {
|
||||
style: Arc<EditorStyle>,
|
||||
}
|
||||
@@ -1465,11 +1464,9 @@ impl EditorElement {
|
||||
line_height: f32,
|
||||
style: &EditorStyle,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
include_root: bool,
|
||||
editor: &mut Editor,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
) -> (f32, Vec<BlockLayout>) {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let scroll_x = snapshot.scroll_anchor.offset.x();
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
@@ -1511,7 +1508,12 @@ impl EditorElement {
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
let id = *id;
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
@@ -1524,7 +1526,7 @@ impl EditorElement {
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
|
||||
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
@@ -1556,7 +1558,7 @@ impl EditorElement {
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JumpIcon>(
|
||||
id.into(),
|
||||
(*id).into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
@@ -1567,9 +1569,9 @@ impl EditorElement {
|
||||
});
|
||||
|
||||
if *starts_new_buffer {
|
||||
let style = &self.style.diagnostic_path_header;
|
||||
let font_size =
|
||||
(style.text_scale_factor * self.style.text.font_size).round();
|
||||
let editor_font_size = style.text.font_size;
|
||||
let style = &style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor * editor_font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
@@ -1605,7 +1607,7 @@ impl EditorElement {
|
||||
.expanded()
|
||||
.into_any_named("path header block")
|
||||
} else {
|
||||
let text_style = self.style.text.clone();
|
||||
let text_style = style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("⋯", text_style))
|
||||
.with_children(jump_icon)
|
||||
@@ -1899,7 +1901,7 @@ impl Element<Editor> for EditorElement {
|
||||
let gutter_padding;
|
||||
let gutter_width;
|
||||
let gutter_margin;
|
||||
if snapshot.mode == EditorMode::Full {
|
||||
if snapshot.show_gutter {
|
||||
let em_width = style.text.em_width(cx.font_cache());
|
||||
gutter_padding = (em_width * style.gutter_padding_factor).round();
|
||||
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
|
||||
@@ -2080,12 +2082,6 @@ impl Element<Editor> for EditorElement {
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
|
||||
.into_iter()
|
||||
.map(|(id, fold)| {
|
||||
@@ -2144,7 +2140,6 @@ impl Element<Editor> for EditorElement {
|
||||
line_height,
|
||||
&style,
|
||||
&line_layouts,
|
||||
include_root,
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
@@ -2888,6 +2883,7 @@ mod tests {
|
||||
position: Anchor::min(),
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -3080,7 +3076,7 @@ mod tests {
|
||||
editor_width: f32,
|
||||
) -> Vec<Invisible> {
|
||||
info!(
|
||||
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
|
||||
"Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
|
||||
);
|
||||
let (_, editor) = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(&input_text, cx);
|
||||
|
||||
@@ -221,6 +221,7 @@ fn show_hover(
|
||||
project: project.clone(),
|
||||
symbol_range: range,
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
})
|
||||
});
|
||||
@@ -253,6 +254,7 @@ fn render_blocks(
|
||||
theme_id: usize,
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
style: &EditorStyle,
|
||||
) -> RenderedInfo {
|
||||
let mut text = String::new();
|
||||
@@ -351,11 +353,13 @@ fn render_blocks(
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
if let CodeBlockKind::Fenced(language) = kind {
|
||||
current_language = language_registry
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.now_or_never()
|
||||
.and_then(Result::ok);
|
||||
.and_then(Result::ok)
|
||||
} else {
|
||||
language.cloned()
|
||||
}
|
||||
}
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
@@ -414,10 +418,6 @@ fn render_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
RenderedInfo {
|
||||
theme_id,
|
||||
text,
|
||||
@@ -524,6 +524,7 @@ pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub symbol_range: Range<Anchor>,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
}
|
||||
|
||||
@@ -559,6 +560,7 @@ impl InfoPopover {
|
||||
style.theme_id,
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
style,
|
||||
)
|
||||
});
|
||||
@@ -588,10 +590,7 @@ impl InfoPopover {
|
||||
MouseRegion::new::<Self>(view_id, region_id, bounds)
|
||||
.on_click::<Editor, _>(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
println!("clicked link {url}");
|
||||
cx.platform().open_url(&url);
|
||||
},
|
||||
move |_, _, cx| cx.platform().open_url(&url),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -906,7 +905,7 @@ mod tests {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec
|
||||
- d
|
||||
"
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
@@ -949,8 +947,7 @@ mod tests {
|
||||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d
|
||||
"
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
@@ -973,9 +970,8 @@ mod tests {
|
||||
|
||||
nine
|
||||
* ten
|
||||
* six
|
||||
"
|
||||
.unindent(),
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
@@ -985,9 +981,8 @@ mod tests {
|
||||
|
||||
nine
|
||||
- ten
|
||||
- six
|
||||
"
|
||||
.unindent(),
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
@@ -1004,7 +999,7 @@ mod tests {
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), &style);
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
|
||||
@@ -196,7 +196,7 @@ impl FollowableItem for Editor {
|
||||
singleton: buffer.is_singleton(),
|
||||
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
|
||||
excerpts,
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
|
||||
scroll_x: scroll_anchor.offset.x(),
|
||||
scroll_y: scroll_anchor.offset.y(),
|
||||
selections: self
|
||||
@@ -253,7 +253,7 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
Event::ScrollPositionChanged { .. } => {
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
|
||||
update.scroll_x = scroll_anchor.offset.x();
|
||||
update.scroll_y = scroll_anchor.offset.y();
|
||||
true
|
||||
@@ -294,7 +294,7 @@ impl FollowableItem for Editor {
|
||||
match event {
|
||||
Event::Edited => true,
|
||||
Event::SelectionsChanged { local } => *local,
|
||||
Event::ScrollPositionChanged { local } => *local,
|
||||
Event::ScrollPositionChanged { local, .. } => *local,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ async fn update_editor_from_message(
|
||||
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
anchor: scroll_top_anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
cx,
|
||||
@@ -510,8 +510,8 @@ impl Item for Editor {
|
||||
};
|
||||
|
||||
let mut scroll_anchor = data.scroll_anchor;
|
||||
if !buffer.can_resolve(&scroll_anchor.top_anchor) {
|
||||
scroll_anchor.top_anchor = buffer.anchor_before(
|
||||
if !buffer.can_resolve(&scroll_anchor.anchor) {
|
||||
scroll_anchor.anchor = buffer.anchor_before(
|
||||
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
|
||||
);
|
||||
}
|
||||
@@ -1231,6 +1231,10 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -312,6 +312,7 @@ mod tests {
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -64,6 +64,9 @@ pub enum Event {
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
ExcerptsEdited {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
Edited,
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
@@ -196,6 +199,13 @@ pub struct MultiBufferBytes<'a> {
|
||||
chunk: &'a [u8],
|
||||
}
|
||||
|
||||
pub struct ReversedMultiBufferBytes<'a> {
|
||||
range: Range<usize>,
|
||||
excerpts: Cursor<'a, Excerpt, usize>,
|
||||
excerpt_bytes: Option<ExcerptBytes<'a>>,
|
||||
chunk: &'a [u8],
|
||||
}
|
||||
|
||||
struct ExcerptChunks<'a> {
|
||||
content_chunks: BufferChunks<'a>,
|
||||
footer_height: usize,
|
||||
@@ -387,6 +397,7 @@ impl MultiBuffer {
|
||||
original_indent_column: u32,
|
||||
}
|
||||
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
|
||||
let mut edited_excerpt_ids = Vec::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
for (ix, (range, new_text)) in edits.enumerate() {
|
||||
let new_text: Arc<str> = new_text.into();
|
||||
@@ -403,6 +414,7 @@ impl MultiBuffer {
|
||||
.start
|
||||
.to_offset(&start_excerpt.buffer)
|
||||
+ start_overshoot;
|
||||
edited_excerpt_ids.push(start_excerpt.id);
|
||||
|
||||
cursor.seek(&range.end, Bias::Right, &());
|
||||
if cursor.item().is_none() && range.end == *cursor.start() {
|
||||
@@ -428,6 +440,7 @@ impl MultiBuffer {
|
||||
original_indent_column,
|
||||
});
|
||||
} else {
|
||||
edited_excerpt_ids.push(end_excerpt.id);
|
||||
let start_excerpt_range = buffer_start
|
||||
..start_excerpt
|
||||
.range
|
||||
@@ -474,6 +487,7 @@ impl MultiBuffer {
|
||||
is_insertion: false,
|
||||
original_indent_column,
|
||||
});
|
||||
edited_excerpt_ids.push(excerpt.id);
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
@@ -546,6 +560,10 @@ impl MultiBuffer {
|
||||
buffer.edit(insertions, insertion_autoindent_mode, cx);
|
||||
})
|
||||
}
|
||||
|
||||
cx.emit(Event::ExcerptsEdited {
|
||||
ids: edited_excerpt_ids,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
@@ -1100,7 +1118,7 @@ impl MultiBuffer {
|
||||
&self,
|
||||
point: T,
|
||||
cx: &AppContext,
|
||||
) -> Option<(ModelHandle<Buffer>, usize)> {
|
||||
) -> Option<(ModelHandle<Buffer>, usize, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let offset = point.to_offset(&snapshot);
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
@@ -1114,7 +1132,7 @@ impl MultiBuffer {
|
||||
let buffer_point = excerpt_start + offset - *cursor.start();
|
||||
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
|
||||
|
||||
(buffer, buffer_point)
|
||||
(buffer, buffer_point, excerpt.id)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1122,7 +1140,7 @@ impl MultiBuffer {
|
||||
&self,
|
||||
range: Range<T>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<(ModelHandle<Buffer>, Range<usize>)> {
|
||||
) -> Vec<(ModelHandle<Buffer>, Range<usize>, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let start = range.start.to_offset(&snapshot);
|
||||
let end = range.end.to_offset(&snapshot);
|
||||
@@ -1147,7 +1165,7 @@ impl MultiBuffer {
|
||||
let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
|
||||
let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
|
||||
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
|
||||
result.push((buffer, start..end));
|
||||
result.push((buffer, start..end, excerpt.id));
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
@@ -1369,7 +1387,7 @@ impl MultiBuffer {
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Arc<Language>> {
|
||||
self.point_to_buffer_offset(point, cx)
|
||||
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
|
||||
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
|
||||
}
|
||||
|
||||
pub fn settings_at<'a, T: ToOffset>(
|
||||
@@ -1377,8 +1395,14 @@ impl MultiBuffer {
|
||||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language = self.language_at(point, cx);
|
||||
language_settings(language.map(|l| l.name()).as_deref(), cx)
|
||||
let mut language = None;
|
||||
let mut file = None;
|
||||
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
language = buffer.language_at(offset);
|
||||
file = buffer.file();
|
||||
}
|
||||
language_settings(language.as_ref(), file, cx)
|
||||
}
|
||||
|
||||
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
|
||||
@@ -1961,7 +1985,6 @@ impl MultiBufferSnapshot {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
MultiBufferBytes {
|
||||
range,
|
||||
excerpts,
|
||||
@@ -1970,6 +1993,33 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reversed_bytes_in_range<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> ReversedMultiBufferBytes {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut excerpts = self.excerpts.cursor::<usize>();
|
||||
excerpts.seek(&range.end, Bias::Left, &());
|
||||
|
||||
let mut chunk = &[][..];
|
||||
let excerpt_bytes = if let Some(excerpt) = excerpts.item() {
|
||||
let mut excerpt_bytes = excerpt.reversed_bytes_in_range(
|
||||
range.start - excerpts.start()..range.end - excerpts.start(),
|
||||
);
|
||||
chunk = excerpt_bytes.next().unwrap_or(&[][..]);
|
||||
Some(excerpt_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ReversedMultiBufferBytes {
|
||||
range,
|
||||
excerpts,
|
||||
excerpt_bytes,
|
||||
chunk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows {
|
||||
let mut result = MultiBufferRows {
|
||||
buffer_row_range: 0..0,
|
||||
@@ -2785,9 +2835,13 @@ impl MultiBufferSnapshot {
|
||||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
self.point_to_buffer_offset(point)
|
||||
.map(|(buffer, offset)| buffer.settings_at(offset, cx))
|
||||
.unwrap_or_else(|| language_settings(None, cx))
|
||||
let mut language = None;
|
||||
let mut file = None;
|
||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
|
||||
language = buffer.language_at(offset);
|
||||
file = buffer.file();
|
||||
}
|
||||
language_settings(language, file, cx)
|
||||
}
|
||||
|
||||
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
|
||||
@@ -3399,6 +3453,26 @@ impl Excerpt {
|
||||
}
|
||||
}
|
||||
|
||||
fn reversed_bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
|
||||
let content_start = self.range.context.start.to_offset(&self.buffer);
|
||||
let bytes_start = content_start + range.start;
|
||||
let bytes_end = content_start + cmp::min(range.end, self.text_summary.len);
|
||||
let footer_height = if self.has_trailing_newline
|
||||
&& range.start <= self.text_summary.len
|
||||
&& range.end > self.text_summary.len
|
||||
{
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end);
|
||||
|
||||
ExcerptBytes {
|
||||
content_bytes,
|
||||
footer_height,
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
|
||||
if text_anchor
|
||||
.cmp(&self.range.context.start, &self.buffer)
|
||||
@@ -3717,6 +3791,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ReversedMultiBufferBytes<'a> {
|
||||
fn consume(&mut self, len: usize) {
|
||||
self.range.end -= len;
|
||||
self.chunk = &self.chunk[..self.chunk.len() - len];
|
||||
|
||||
if !self.range.is_empty() && self.chunk.is_empty() {
|
||||
if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) {
|
||||
self.chunk = chunk;
|
||||
} else {
|
||||
self.excerpts.next(&());
|
||||
if let Some(excerpt) = self.excerpts.item() {
|
||||
let mut excerpt_bytes =
|
||||
excerpt.bytes_in_range(0..self.range.end - self.excerpts.start());
|
||||
self.chunk = excerpt_bytes.next().unwrap();
|
||||
self.excerpt_bytes = Some(excerpt_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> io::Read for ReversedMultiBufferBytes<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let len = cmp::min(buf.len(), self.chunk.len());
|
||||
buf[..len].copy_from_slice(&self.chunk[..len]);
|
||||
buf[..len].reverse();
|
||||
if len > 0 {
|
||||
self.consume(len);
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for ExcerptBytes<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
@@ -5090,7 +5196,7 @@ mod tests {
|
||||
.range_to_buffer_ranges(start_ix..end_ix, cx);
|
||||
let excerpted_buffers_text = excerpted_buffer_ranges
|
||||
.iter()
|
||||
.map(|(buffer, buffer_range)| {
|
||||
.map(|(buffer, buffer_range, _)| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.text_for_range(buffer_range.clone())
|
||||
@@ -5237,7 +5343,7 @@ mod tests {
|
||||
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
|
||||
|
||||
// An undo in the multibuffer undoes the multibuffer transaction
|
||||
// and also any individual buffer edits that have occured since
|
||||
// and also any individual buffer edits that have occurred since
|
||||
// that transaction.
|
||||
multibuffer.undo(cx);
|
||||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||||
|
||||
@@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool);
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ScrollAnchor {
|
||||
pub offset: Vector2F,
|
||||
pub top_anchor: Anchor,
|
||||
pub anchor: Anchor,
|
||||
}
|
||||
|
||||
impl ScrollAnchor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
offset: Vector2F::zero(),
|
||||
top_anchor: Anchor::min(),
|
||||
anchor: Anchor::min(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
|
||||
let mut scroll_position = self.offset;
|
||||
if self.top_anchor != Anchor::min() {
|
||||
let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32;
|
||||
if self.anchor != Anchor::min() {
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
|
||||
scroll_position.set_y(scroll_top + scroll_position.y());
|
||||
} else {
|
||||
scroll_position.set_y(0.);
|
||||
@@ -59,7 +59,7 @@ impl ScrollAnchor {
|
||||
}
|
||||
|
||||
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
|
||||
self.top_anchor.to_point(buffer).row
|
||||
self.anchor.to_point(buffer).row
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,13 +173,14 @@ impl ScrollManager {
|
||||
scroll_position: Vector2F,
|
||||
map: &DisplaySnapshot,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
workspace_id: Option<i64>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let (new_anchor, top_row) = if scroll_position.y() <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
top_anchor: Anchor::min(),
|
||||
anchor: Anchor::min(),
|
||||
offset: scroll_position.max(vec2f(0., 0.)),
|
||||
},
|
||||
0,
|
||||
@@ -193,7 +194,7 @@ impl ScrollManager {
|
||||
|
||||
(
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
offset: vec2f(
|
||||
scroll_position.x(),
|
||||
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
|
||||
@@ -203,7 +204,7 @@ impl ScrollManager {
|
||||
)
|
||||
};
|
||||
|
||||
self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
|
||||
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
|
||||
}
|
||||
|
||||
fn set_anchor(
|
||||
@@ -211,11 +212,12 @@ impl ScrollManager {
|
||||
anchor: ScrollAnchor,
|
||||
top_row: u32,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
workspace_id: Option<i64>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.anchor = anchor;
|
||||
cx.emit(Event::ScrollPositionChanged { local });
|
||||
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
|
||||
self.show_scrollbar(cx);
|
||||
self.autoscroll_request.take();
|
||||
if let Some(workspace_id) = workspace_id {
|
||||
@@ -296,21 +298,28 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
||||
self.set_scroll_position_internal(scroll_position, true, cx);
|
||||
self.set_scroll_position_internal(scroll_position, true, false, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_position_internal(
|
||||
&mut self,
|
||||
scroll_position: Vector2F,
|
||||
local: bool,
|
||||
autoscroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
self.scroll_manager
|
||||
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
|
||||
self.scroll_manager.set_scroll_position(
|
||||
scroll_position,
|
||||
&map,
|
||||
local,
|
||||
autoscroll,
|
||||
workspace_id,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
@@ -322,11 +331,11 @@ impl Editor {
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_anchor_remote(
|
||||
@@ -337,11 +346,11 @@ impl Editor {
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
|
||||
@@ -377,7 +386,7 @@ impl Editor {
|
||||
let screen_top = self
|
||||
.scroll_manager
|
||||
.anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
if screen_top > newest_head {
|
||||
@@ -408,7 +417,7 @@ impl Editor {
|
||||
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
|
||||
let scroll_anchor = ScrollAnchor {
|
||||
offset: Vector2F::new(x, y),
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
};
|
||||
self.set_scroll_anchor(scroll_anchor, cx);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ impl Editor {
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
@@ -113,7 +113,7 @@ impl Editor {
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
@@ -143,7 +143,7 @@ impl Editor {
|
||||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
|
||||
@@ -136,23 +136,23 @@ impl Editor {
|
||||
|
||||
if target_top < start_row {
|
||||
scroll_position.set_y(target_top);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
} else if target_bottom >= end_row {
|
||||
scroll_position.set_y(target_bottom - visible_lines);
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
}
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.set_y((first_cursor_top).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ impl SelectionsCollection {
|
||||
count
|
||||
}
|
||||
|
||||
/// The non-pending, non-overlapping selections. There could still be a pending
|
||||
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
|
||||
/// selections over Anchors.
|
||||
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
|
||||
self.disjoint.clone()
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ pub fn marked_display_snapshot(
|
||||
}
|
||||
|
||||
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
|
||||
let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), umarked_text);
|
||||
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::{env, fmt::Display};
|
||||
use sysinfo::{System, SystemExt};
|
||||
use util::channel::ReleaseChannel;
|
||||
|
||||
// TODO: Move this file out of feedback and into a more general place
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SystemSpecs {
|
||||
#[serde(serialize_with = "serialize_app_version")]
|
||||
|
||||
@@ -14,6 +14,8 @@ lsp = { path = "../lsp" }
|
||||
rope = { path = "../rope" }
|
||||
util = { path = "../util" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -29,10 +29,12 @@ use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use repository::{FakeGitRepositoryState, GitFileStatus};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Weak;
|
||||
|
||||
lazy_static! {
|
||||
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||
static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -77,13 +79,13 @@ impl LineEnding {
|
||||
}
|
||||
|
||||
pub fn normalize(text: &mut String) {
|
||||
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
|
||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
|
||||
*text = replaced;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
|
||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
|
||||
replaced.into()
|
||||
} else {
|
||||
text
|
||||
@@ -501,6 +503,11 @@ impl FakeFsState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
lazy_static! {
|
||||
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeFs {
|
||||
pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
|
||||
@@ -619,7 +626,7 @@ impl FakeFs {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
|
||||
pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState),
|
||||
{
|
||||
@@ -633,18 +640,22 @@ impl FakeFs {
|
||||
|
||||
f(&mut repo_state);
|
||||
|
||||
state.emit_event([dot_git]);
|
||||
if emit_git_event {
|
||||
state.emit_event([dot_git]);
|
||||
}
|
||||
} else {
|
||||
panic!("not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
|
||||
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.branch_name = branch.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.index_contents.clear();
|
||||
state.index_contents.extend(
|
||||
head_state
|
||||
@@ -654,8 +665,32 @@ impl FakeFs {
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
|
||||
self.with_git_state(dot_git, |state| {
|
||||
pub fn set_status_for_repo_via_working_copy_change(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, false, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, content)| ((**path).into(), content.clone())),
|
||||
);
|
||||
});
|
||||
self.state.lock().emit_event(
|
||||
statuses
|
||||
.iter()
|
||||
.map(|(path, _)| dot_git.parent().unwrap().join(path)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_status_for_repo_via_git_operation(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
statuses: &[(&Path, GitFileStatus)],
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.worktree_statuses.clear();
|
||||
state.worktree_statuses.extend(
|
||||
statuses
|
||||
@@ -665,7 +700,7 @@ impl FakeFs {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> Vec<PathBuf> {
|
||||
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
|
||||
@@ -675,12 +710,18 @@ impl FakeFs {
|
||||
queue.push_back((path.join(name), entry.clone()));
|
||||
}
|
||||
}
|
||||
result.push(path);
|
||||
if include_dot_git
|
||||
|| !path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == *FS_DOT_GIT)
|
||||
{
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn directories(&self) -> Vec<PathBuf> {
|
||||
pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
|
||||
@@ -689,7 +730,13 @@ impl FakeFs {
|
||||
for (name, entry) in entries {
|
||||
queue.push_back((path.join(name), entry.clone()));
|
||||
}
|
||||
result.push(path);
|
||||
if include_dot_git
|
||||
|| !path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == *FS_DOT_GIT)
|
||||
{
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git2::ErrorCode;
|
||||
use parking_lot::Mutex;
|
||||
use rpc::proto;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
@@ -24,7 +26,7 @@ pub trait GitRepository: Send {
|
||||
|
||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn GitRepository {
|
||||
@@ -91,9 +93,18 @@ impl GitRepository for LibGitRepository {
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
let status = self.status_file(path).log_err()?;
|
||||
read_status(status)
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
|
||||
let status = self.status_file(path);
|
||||
match status {
|
||||
Ok(status) => Ok(read_status(status)),
|
||||
Err(e) => {
|
||||
if e.code() == ErrorCode::NotFound {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,9 +166,9 @@ impl GitRepository for FakeGitRepository {
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
|
||||
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
|
||||
let state = self.state.lock();
|
||||
state.worktree_statuses.get(path).cloned()
|
||||
Ok(state.worktree_statuses.get(path).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +208,51 @@ pub enum GitFileStatus {
|
||||
Conflict,
|
||||
}
|
||||
|
||||
impl GitFileStatus {
|
||||
pub fn merge(
|
||||
this: Option<GitFileStatus>,
|
||||
other: Option<GitFileStatus>,
|
||||
prefer_other: bool,
|
||||
) -> Option<GitFileStatus> {
|
||||
if prefer_other {
|
||||
return other;
|
||||
} else {
|
||||
match (this, other) {
|
||||
(Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
|
||||
Some(GitFileStatus::Conflict)
|
||||
}
|
||||
(Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
|
||||
Some(GitFileStatus::Modified)
|
||||
}
|
||||
(Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
|
||||
Some(GitFileStatus::Added)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
|
||||
git_status.and_then(|status| {
|
||||
proto::GitStatus::from_i32(status).map(|status| match status {
|
||||
proto::GitStatus::Added => GitFileStatus::Added,
|
||||
proto::GitStatus::Modified => GitFileStatus::Modified,
|
||||
proto::GitStatus::Conflict => GitFileStatus::Conflict,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(self) -> i32 {
|
||||
match self {
|
||||
GitFileStatus::Added => proto::GitStatus::Added as i32,
|
||||
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
|
||||
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
|
||||
pub struct RepoPath(PathBuf);
|
||||
pub struct RepoPath(pub PathBuf);
|
||||
|
||||
impl RepoPath {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
|
||||
@@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] }
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
||||
bindgen = "0.65.1"
|
||||
cc = "1.0.67"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -6335,9 +6335,9 @@ mod tests {
|
||||
#[crate::test(self)]
|
||||
async fn test_labeled_tasks(cx: &mut TestAppContext) {
|
||||
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
|
||||
let (mut sender, mut reciever) = postage::oneshot::channel::<()>();
|
||||
let (mut sender, mut receiver) = postage::oneshot::channel::<()>();
|
||||
let task = cx
|
||||
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await }));
|
||||
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await }));
|
||||
|
||||
assert_eq!(
|
||||
Some("Test Label"),
|
||||
|
||||
@@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let focused_view_id = self.window.focused_view_id?;
|
||||
self.window
|
||||
.rendered_views
|
||||
.get(&root_view_id)?
|
||||
.get(&focused_view_id)?
|
||||
.rect_for_text_range(range_utf16, self)
|
||||
.log_err()
|
||||
.flatten()
|
||||
|
||||
@@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler {
|
||||
|
||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||
self.app
|
||||
.borrow_mut()
|
||||
.update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
|
||||
.borrow()
|
||||
.read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct StateInner {
|
||||
scroll_to: Option<ScrollTarget>,
|
||||
}
|
||||
|
||||
pub struct LayoutState<V: View> {
|
||||
pub struct UniformListLayoutState<V: View> {
|
||||
scroll_max: f32,
|
||||
item_height: f32,
|
||||
items: Vec<AnyElement<V>>,
|
||||
@@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
|
||||
}
|
||||
|
||||
impl<V: View> Element<V> for UniformList<V> {
|
||||
type LayoutState = LayoutState<V>;
|
||||
type LayoutState = UniformListLayoutState<V>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
@@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
|
||||
|
||||
let no_items = (
|
||||
constraint.min,
|
||||
LayoutState {
|
||||
UniformListLayoutState {
|
||||
item_height: 0.,
|
||||
scroll_max: 0.,
|
||||
items: Default::default(),
|
||||
@@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
|
||||
|
||||
(
|
||||
size,
|
||||
LayoutState {
|
||||
UniformListLayoutState {
|
||||
item_height,
|
||||
scroll_max,
|
||||
items,
|
||||
|
||||
@@ -67,7 +67,7 @@ impl KeymapMatcher {
|
||||
/// MatchResult::Pending =>
|
||||
/// There exist bindings which are still waiting for more keys.
|
||||
/// MatchResult::Complete(matches) =>
|
||||
/// 1 or more bindings have recieved the necessary key presses.
|
||||
/// 1 or more bindings have received the necessary key presses.
|
||||
/// The order of the matched actions is by position of the matching first,
|
||||
// and order in the keymap second.
|
||||
pub fn push_keystroke(
|
||||
|
||||
@@ -55,7 +55,7 @@ serde_json.workspace = true
|
||||
similar = "1.3"
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-typescript = { version = "*", optional = true }
|
||||
unicase = "2.6"
|
||||
@@ -72,6 +72,8 @@ ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
indoc.workspace = true
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
tree-sitter-embedded-template = "*"
|
||||
tree-sitter-html = "*"
|
||||
tree-sitter-javascript = "*"
|
||||
@@ -81,4 +83,3 @@ tree-sitter-rust = "*"
|
||||
tree-sitter-python = "*"
|
||||
tree-sitter-typescript = "*"
|
||||
tree-sitter-ruby = "*"
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::{
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
outline::OutlineItem,
|
||||
syntax_map::{
|
||||
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
|
||||
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot,
|
||||
ToTreeSitterPoint,
|
||||
},
|
||||
CodeLabel, LanguageScope, Outline,
|
||||
};
|
||||
@@ -216,6 +217,11 @@ pub trait File: Send + Sync {
|
||||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
|
||||
|
||||
/// Returns the id of the worktree to which this file belongs.
|
||||
///
|
||||
/// This is needed for looking up project-specific settings.
|
||||
fn worktree_id(&self) -> usize;
|
||||
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
@@ -1802,8 +1808,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
|
||||
let language_name = self.language_at(position).map(|language| language.name());
|
||||
let settings = language_settings(language_name.as_deref(), cx);
|
||||
let settings = language_settings(self.language_at(position), self.file(), cx);
|
||||
if settings.hard_tabs {
|
||||
IndentSize::tab()
|
||||
} else {
|
||||
@@ -2112,12 +2117,20 @@ impl BufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
|
||||
self.syntax.layers_for_range(0..self.len(), &self.text)
|
||||
}
|
||||
|
||||
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
|
||||
let offset = position.to_offset(self);
|
||||
self.syntax
|
||||
.layers_for_range(offset..offset, &self.text)
|
||||
.filter(|l| l.node.end_byte() > offset)
|
||||
.filter(|l| l.node().end_byte() > offset)
|
||||
.last()
|
||||
}
|
||||
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
self.syntax_layer_at(position)
|
||||
.map(|info| info.language)
|
||||
.or(self.language.as_ref())
|
||||
}
|
||||
@@ -2127,8 +2140,7 @@ impl BufferSnapshot {
|
||||
position: D,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language = self.language_at(position);
|
||||
language_settings(language.map(|l| l.name()).as_deref(), cx)
|
||||
language_settings(self.language_at(position), self.file.as_ref(), cx)
|
||||
}
|
||||
|
||||
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
|
||||
@@ -2137,7 +2149,7 @@ impl BufferSnapshot {
|
||||
if let Some(layer_info) = self
|
||||
.syntax
|
||||
.layers_for_range(offset..offset, &self.text)
|
||||
.filter(|l| l.node.end_byte() > offset)
|
||||
.filter(|l| l.node().end_byte() > offset)
|
||||
.last()
|
||||
{
|
||||
Some(LanguageScope {
|
||||
@@ -2185,7 +2197,7 @@ impl BufferSnapshot {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut result: Option<Range<usize>> = None;
|
||||
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
|
||||
let mut cursor = layer.node.walk();
|
||||
let mut cursor = layer.node().walk();
|
||||
|
||||
// Descend to the first leaf that touches the start of the range,
|
||||
// and if the range is non-empty, extends beyond the start.
|
||||
@@ -2250,7 +2262,7 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
self.outline_items_containing(0..self.len(), theme)
|
||||
self.outline_items_containing(0..self.len(), true, theme)
|
||||
.map(Outline::new)
|
||||
}
|
||||
|
||||
@@ -2262,6 +2274,7 @@ impl BufferSnapshot {
|
||||
let position = position.to_offset(self);
|
||||
let mut items = self.outline_items_containing(
|
||||
position.saturating_sub(1)..self.len().min(position + 1),
|
||||
false,
|
||||
theme,
|
||||
)?;
|
||||
let mut prev_depth = None;
|
||||
@@ -2276,6 +2289,7 @@ impl BufferSnapshot {
|
||||
fn outline_items_containing(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
include_extra_context: bool,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<Vec<OutlineItem<Anchor>>> {
|
||||
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|
||||
@@ -2310,7 +2324,10 @@ impl BufferSnapshot {
|
||||
let node_is_name;
|
||||
if capture.index == config.name_capture_ix {
|
||||
node_is_name = true;
|
||||
} else if Some(capture.index) == config.context_capture_ix {
|
||||
} else if Some(capture.index) == config.context_capture_ix
|
||||
|| (Some(capture.index) == config.extra_context_capture_ix
|
||||
&& include_extra_context)
|
||||
{
|
||||
node_is_name = false;
|
||||
} else {
|
||||
continue;
|
||||
@@ -2337,10 +2354,12 @@ impl BufferSnapshot {
|
||||
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
|
||||
true,
|
||||
);
|
||||
let mut last_buffer_range_end = 0;
|
||||
for (buffer_range, is_name) in buffer_ranges {
|
||||
if !text.is_empty() {
|
||||
if !text.is_empty() && buffer_range.start > last_buffer_range_end {
|
||||
text.push(' ');
|
||||
}
|
||||
last_buffer_range_end = buffer_range.end;
|
||||
if is_name {
|
||||
let mut start = text.len();
|
||||
let end = start + buffer_range.len();
|
||||
|
||||
@@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
|
||||
let language = javascript_lang()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(function_declaration
|
||||
"function" @context
|
||||
name: (_) @name
|
||||
parameters: (formal_parameters
|
||||
"(" @context.extra
|
||||
")" @context.extra)) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let text = r#"
|
||||
function a() {}
|
||||
function b(c) {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
// extra context nodes are included in the outline.
|
||||
let outline = snapshot.outline(None).unwrap();
|
||||
assert_eq!(
|
||||
outline
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("function a()", 0), ("function b( )", 0),]
|
||||
);
|
||||
|
||||
// extra context nodes do not appear in breadcrumbs.
|
||||
let symbols = snapshot.symbols_containing(3, None).unwrap();
|
||||
assert_eq!(
|
||||
symbols
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("function a", 0)]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
||||
let text = r#"
|
||||
@@ -2196,7 +2242,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
|
||||
layers[0].node.to_sexp()
|
||||
layers[0].node().to_sexp()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Not, Range},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::{
|
||||
@@ -57,6 +57,7 @@ pub use buffer::*;
|
||||
pub use diagnostic_set::DiagnosticEntry;
|
||||
pub use lsp::LanguageServerId;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@@ -455,6 +456,7 @@ struct OutlineConfig {
|
||||
item_capture_ix: u32,
|
||||
name_capture_ix: u32,
|
||||
context_capture_ix: Option<u32>,
|
||||
extra_context_capture_ix: Option<u32>,
|
||||
}
|
||||
|
||||
struct InjectionConfig {
|
||||
@@ -500,6 +502,7 @@ struct AvailableLanguage {
|
||||
grammar: tree_sitter::Language,
|
||||
lsp_adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
get_queries: fn(&str) -> LanguageQueries,
|
||||
loaded: bool,
|
||||
}
|
||||
|
||||
pub struct LanguageRegistry {
|
||||
@@ -527,6 +530,7 @@ struct LanguageRegistryState {
|
||||
subscription: (watch::Sender<()>, watch::Receiver<()>),
|
||||
theme: Option<Arc<Theme>>,
|
||||
version: usize,
|
||||
reload_count: usize,
|
||||
}
|
||||
|
||||
pub struct PendingLanguageServer {
|
||||
@@ -547,6 +551,7 @@ impl LanguageRegistry {
|
||||
subscription: watch::channel(),
|
||||
theme: Default::default(),
|
||||
version: 0,
|
||||
reload_count: 0,
|
||||
}),
|
||||
language_server_download_dir: None,
|
||||
lsp_binary_statuses_tx,
|
||||
@@ -566,6 +571,14 @@ impl LanguageRegistry {
|
||||
self.executor = Some(executor);
|
||||
}
|
||||
|
||||
/// Clear out all of the loaded languages and reload them from scratch.
|
||||
///
|
||||
/// This is useful in development, when queries have changed.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn reload(&self) {
|
||||
self.state.write().reload();
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&self,
|
||||
path: &'static str,
|
||||
@@ -582,6 +595,7 @@ impl LanguageRegistry {
|
||||
grammar,
|
||||
lsp_adapters,
|
||||
get_queries,
|
||||
loaded: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -590,7 +604,7 @@ impl LanguageRegistry {
|
||||
let mut result = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.map(|l| l.config.name.to_string())
|
||||
.filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
|
||||
.chain(state.languages.iter().map(|l| l.config.name.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
|
||||
@@ -603,6 +617,7 @@ impl LanguageRegistry {
|
||||
state
|
||||
.available_languages
|
||||
.iter()
|
||||
.filter(|l| !l.loaded)
|
||||
.flat_map(|l| l.lsp_adapters.clone())
|
||||
.chain(
|
||||
state
|
||||
@@ -639,10 +654,17 @@ impl LanguageRegistry {
|
||||
self.state.read().subscription.1.clone()
|
||||
}
|
||||
|
||||
/// The number of times that the registry has been changed,
|
||||
/// by adding languages or reloading.
|
||||
pub fn version(&self) -> usize {
|
||||
self.state.read().version
|
||||
}
|
||||
|
||||
/// The number of times that the registry has been reloaded.
|
||||
pub fn reload_count(&self) -> usize {
|
||||
self.state.read().reload_count
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: Arc<Theme>) {
|
||||
let mut state = self.state.write();
|
||||
state.theme = Some(theme.clone());
|
||||
@@ -721,7 +743,7 @@ impl LanguageRegistry {
|
||||
if let Some(language) = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.find(|l| callback(&l.config))
|
||||
.find(|l| !l.loaded && callback(&l.config))
|
||||
.cloned()
|
||||
{
|
||||
let txs = state
|
||||
@@ -743,9 +765,7 @@ impl LanguageRegistry {
|
||||
let language = Arc::new(language);
|
||||
let mut state = this.state.write();
|
||||
state.add(language.clone());
|
||||
state
|
||||
.available_languages
|
||||
.retain(|language| language.id != id);
|
||||
state.mark_language_loaded(id);
|
||||
if let Some(mut txs) = state.loading_languages.remove(&id) {
|
||||
for tx in txs.drain(..) {
|
||||
let _ = tx.send(Ok(language.clone()));
|
||||
@@ -753,10 +773,9 @@ impl LanguageRegistry {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to load language {name} - {err}");
|
||||
let mut state = this.state.write();
|
||||
state
|
||||
.available_languages
|
||||
.retain(|language| language.id != id);
|
||||
state.mark_language_loaded(id);
|
||||
if let Some(mut txs) = state.loading_languages.remove(&id) {
|
||||
for tx in txs.drain(..) {
|
||||
let _ = tx.send(Err(anyhow!(
|
||||
@@ -905,6 +924,28 @@ impl LanguageRegistryState {
|
||||
self.version += 1;
|
||||
*self.subscription.0.borrow_mut() = ();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn reload(&mut self) {
|
||||
self.languages.clear();
|
||||
self.version += 1;
|
||||
self.reload_count += 1;
|
||||
for language in &mut self.available_languages {
|
||||
language.loaded = false;
|
||||
}
|
||||
*self.subscription.0.borrow_mut() = ();
|
||||
}
|
||||
|
||||
/// Mark the given language a having been loaded, so that the
|
||||
/// language registry won't try to load it again.
|
||||
fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
|
||||
for language in &mut self.available_languages {
|
||||
if language.id == id {
|
||||
language.loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -1021,34 +1062,22 @@ impl Language {
|
||||
|
||||
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
|
||||
if let Some(query) = queries.highlights {
|
||||
self = self
|
||||
.with_highlights_query(query.as_ref())
|
||||
.expect("failed to evaluate highlights query");
|
||||
self = self.with_highlights_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.brackets {
|
||||
self = self
|
||||
.with_brackets_query(query.as_ref())
|
||||
.expect("failed to load brackets query");
|
||||
self = self.with_brackets_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.indents {
|
||||
self = self
|
||||
.with_indents_query(query.as_ref())
|
||||
.expect("failed to load indents query");
|
||||
self = self.with_indents_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.outline {
|
||||
self = self
|
||||
.with_outline_query(query.as_ref())
|
||||
.expect("failed to load outline query");
|
||||
self = self.with_outline_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.injections {
|
||||
self = self
|
||||
.with_injection_query(query.as_ref())
|
||||
.expect("failed to load injection query");
|
||||
self = self.with_injection_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.overrides {
|
||||
self = self
|
||||
.with_override_query(query.as_ref())
|
||||
.expect("failed to load override query");
|
||||
self = self.with_override_query(query.as_ref())?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
@@ -1064,12 +1093,14 @@ impl Language {
|
||||
let mut item_capture_ix = None;
|
||||
let mut name_capture_ix = None;
|
||||
let mut context_capture_ix = None;
|
||||
let mut extra_context_capture_ix = None;
|
||||
get_capture_indices(
|
||||
&query,
|
||||
&mut [
|
||||
("item", &mut item_capture_ix),
|
||||
("name", &mut name_capture_ix),
|
||||
("context", &mut context_capture_ix),
|
||||
("context.extra", &mut extra_context_capture_ix),
|
||||
],
|
||||
);
|
||||
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
|
||||
@@ -1078,6 +1109,7 @@ impl Language {
|
||||
item_capture_ix,
|
||||
name_capture_ix,
|
||||
context_capture_ix,
|
||||
extra_context_capture_ix,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::{File, Language};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use globset::GlobMatcher;
|
||||
@@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
|
||||
settings::register::<AllLanguageSettings>(cx);
|
||||
}
|
||||
|
||||
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
|
||||
settings::get::<AllLanguageSettings>(cx).language(language)
|
||||
pub fn language_settings<'a>(
|
||||
language: Option<&Arc<Language>>,
|
||||
file: Option<&Arc<dyn File>>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language_name = language.map(|l| l.name());
|
||||
all_language_settings(file, cx).language(language_name.as_deref())
|
||||
}
|
||||
|
||||
pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
|
||||
settings::get::<AllLanguageSettings>(cx)
|
||||
pub fn all_language_settings<'a>(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a AllLanguageSettings {
|
||||
let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
|
||||
settings::get_local(location, cx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -41,6 +51,7 @@ pub struct LanguageSettings {
|
||||
pub enable_language_server: bool,
|
||||
pub show_copilot_suggestions: bool,
|
||||
pub show_whitespaces: ShowWhitespaceSetting,
|
||||
pub extend_comment_on_newline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -85,6 +96,8 @@ pub struct LanguageSettingsContent {
|
||||
pub show_copilot_suggestions: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_whitespaces: Option<ShowWhitespaceSetting>,
|
||||
#[serde(default)]
|
||||
pub extend_comment_on_newline: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -155,7 +168,7 @@ impl AllLanguageSettings {
|
||||
.any(|glob| glob.is_match(path))
|
||||
}
|
||||
|
||||
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
|
||||
pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
|
||||
if !self.copilot.feature_enabled {
|
||||
return false;
|
||||
}
|
||||
@@ -166,7 +179,8 @@ impl AllLanguageSettings {
|
||||
}
|
||||
}
|
||||
|
||||
self.language(language_name).show_copilot_suggestions
|
||||
self.language(language.map(|l| l.name()).as_deref())
|
||||
.show_copilot_suggestions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +267,7 @@ impl settings::Setting for AllLanguageSettings {
|
||||
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
|
||||
|
||||
// Create a schema for a 'languages overrides' object, associating editor
|
||||
// settings with specific langauges.
|
||||
// settings with specific languages.
|
||||
assert!(root_schema
|
||||
.definitions
|
||||
.contains_key("LanguageSettingsContent"));
|
||||
@@ -329,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
src.show_copilot_suggestions,
|
||||
);
|
||||
merge(&mut settings.show_whitespaces, src.show_whitespaces);
|
||||
|
||||
merge(
|
||||
&mut settings.extend_comment_on_newline,
|
||||
src.extend_comment_on_newline,
|
||||
);
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1199
crates/language/src/syntax_map/syntax_map_tests.rs
Normal file
1199
crates/language/src/syntax_map/syntax_map_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "lsp_log"
|
||||
name = "language_tools"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/lsp_log.rs"
|
||||
path = "src/language_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
@@ -22,9 +22,12 @@ lsp = { path = "../lsp" }
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
anyhow.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
unindent.workspace = true
|
||||
15
crates/language_tools/src/language_tools.rs
Normal file
15
crates/language_tools/src/language_tools.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod lsp_log;
|
||||
mod syntax_tree_view;
|
||||
|
||||
#[cfg(test)]
|
||||
mod lsp_log_tests;
|
||||
|
||||
use gpui::AppContext;
|
||||
|
||||
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
|
||||
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
lsp_log::init(cx);
|
||||
syntax_tree_view::init(cx);
|
||||
}
|
||||
786
crates/language_tools/src/lsp_log.rs
Normal file
786
crates/language_tools/src/lsp_log.rs
Normal file
@@ -0,0 +1,786 @@
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{
|
||||
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
|
||||
ParentElement, Stack,
|
||||
},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
|
||||
ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageServerId, LanguageServerName};
|
||||
use project::{Project, Worktree};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use theme::{ui, Theme};
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchableItem, SearchableItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
|
||||
};
|
||||
|
||||
const SEND_LINE: &str = "// Send:\n";
|
||||
const RECEIVE_LINE: &str = "// Receive:\n";
|
||||
|
||||
pub struct LogStore {
|
||||
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
||||
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
servers: HashMap<LanguageServerId, LanguageServerState>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
struct LanguageServerState {
|
||||
log_buffer: ModelHandle<Buffer>,
|
||||
rpc_state: Option<LanguageServerRpcState>,
|
||||
}
|
||||
|
||||
struct LanguageServerRpcState {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
_subscription: lsp::Subscription,
|
||||
}
|
||||
|
||||
pub struct LspLogView {
|
||||
pub(crate) editor: ViewHandle<Editor>,
|
||||
log_store: ModelHandle<LogStore>,
|
||||
current_server_id: Option<LanguageServerId>,
|
||||
is_showing_rpc_trace: bool,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
pub struct LspLogToolbarItemView {
|
||||
log_view: Option<ViewHandle<LspLogView>>,
|
||||
menu_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum MessageKind {
|
||||
Send,
|
||||
Receive,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct LogMenuItem {
|
||||
pub server_id: LanguageServerId,
|
||||
pub server_name: LanguageServerName,
|
||||
pub worktree: ModelHandle<Worktree>,
|
||||
pub rpc_trace_enabled: bool,
|
||||
pub rpc_trace_selected: bool,
|
||||
pub logs_selected: bool,
|
||||
}
|
||||
|
||||
actions!(debug, [OpenLanguageServerLogs]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let log_store = cx.add_model(|cx| LogStore::new(cx));
|
||||
|
||||
cx.subscribe_global::<WorkspaceCreated, _>({
|
||||
let log_store = log_store.clone();
|
||||
move |event, cx| {
|
||||
let workspace = &event.0;
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
if project.read(cx).is_local() {
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(&project, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() {
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| {
|
||||
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
pub fn new(cx: &mut ModelContext<Self>) -> Self {
|
||||
let (io_tx, mut io_rx) = mpsc::unbounded();
|
||||
let this = Self {
|
||||
projects: HashMap::default(),
|
||||
io_tx,
|
||||
};
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
message.push('\n');
|
||||
this.on_io(project, server_id, is_output, &message, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
|
||||
use project::Event::*;
|
||||
|
||||
let weak_project = project.downgrade();
|
||||
self.projects.insert(
|
||||
weak_project,
|
||||
ProjectState {
|
||||
servers: HashMap::default(),
|
||||
_subscriptions: [
|
||||
cx.observe_release(&project, move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
}),
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
LanguageServerAdded(id) => {
|
||||
this.add_language_server(&project, *id, cx);
|
||||
}
|
||||
LanguageServerRemoved(id) => {
|
||||
this.remove_language_server(&project, *id, cx);
|
||||
}
|
||||
LanguageServerLog(id, message) => {
|
||||
this.add_language_server_log(&project, *id, message, cx);
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn add_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
let project_state = self.projects.get_mut(&project.downgrade())?;
|
||||
Some(
|
||||
project_state
|
||||
.servers
|
||||
.entry(id)
|
||||
.or_insert_with(|| {
|
||||
cx.notify();
|
||||
LanguageServerState {
|
||||
rpc_state: None,
|
||||
log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
|
||||
}
|
||||
})
|
||||
.log_buffer
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn add_language_server_log(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
id: LanguageServerId,
|
||||
message: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let buffer = self.add_language_server(&project, id, cx)?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let len = buffer.len();
|
||||
let has_newline = message.ends_with("\n");
|
||||
buffer.edit([(len..len, message)], None, cx);
|
||||
if !has_newline {
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, "\n")], None, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let project_state = self.projects.get_mut(&project.downgrade())?;
|
||||
project_state.servers.remove(&id);
|
||||
cx.notify();
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn log_buffer_for_server(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
let weak_project = project.downgrade();
|
||||
let project_state = self.projects.get(&weak_project)?;
|
||||
let server_state = project_state.servers.get(&server_id)?;
|
||||
Some(server_state.log_buffer.clone())
|
||||
}
|
||||
|
||||
pub fn enable_rpc_trace_for_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
let weak_project = project.downgrade();
|
||||
let project_state = self.projects.get_mut(&weak_project)?;
|
||||
let server_state = project_state.servers.get_mut(&server_id)?;
|
||||
let server = project.read(cx).language_server_for_id(server_id)?;
|
||||
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
|
||||
let io_tx = self.io_tx.clone();
|
||||
let language = project.read(cx).languages().language_for_name("JSON");
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
|
||||
cx.spawn_weak({
|
||||
let buffer = buffer.clone();
|
||||
|_, mut cx| async move {
|
||||
let language = language.await.ok();
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
LanguageServerRpcState {
|
||||
buffer,
|
||||
last_message_kind: None,
|
||||
_subscription: server.on_io(move |is_received, json| {
|
||||
io_tx
|
||||
.unbounded_send((weak_project, server_id, is_received, json.to_string()))
|
||||
.ok();
|
||||
}),
|
||||
}
|
||||
});
|
||||
Some(rpc_state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn disable_rpc_trace_for_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
_: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let project = project.downgrade();
|
||||
let project_state = self.projects.get_mut(&project)?;
|
||||
let server_state = project_state.servers.get_mut(&server_id)?;
|
||||
server_state.rpc_state.take();
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn on_io(
|
||||
&mut self,
|
||||
project: WeakModelHandle<Project>,
|
||||
language_server_id: LanguageServerId,
|
||||
is_received: bool,
|
||||
message: &str,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let state = self
|
||||
.projects
|
||||
.get_mut(&project)?
|
||||
.servers
|
||||
.get_mut(&language_server_id)?
|
||||
.rpc_state
|
||||
.as_mut()?;
|
||||
state.buffer.update(cx, |buffer, cx| {
|
||||
let kind = if is_received {
|
||||
MessageKind::Receive
|
||||
} else {
|
||||
MessageKind::Send
|
||||
};
|
||||
if state.last_message_kind != Some(kind) {
|
||||
let len = buffer.len();
|
||||
let line = match kind {
|
||||
MessageKind::Send => SEND_LINE,
|
||||
MessageKind::Receive => RECEIVE_LINE,
|
||||
};
|
||||
buffer.edit([(len..len, line)], None, cx);
|
||||
state.last_message_kind = Some(kind);
|
||||
}
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, message)], None, cx);
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LspLogView {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
log_store: ModelHandle<LogStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let server_id = log_store
|
||||
.read(cx)
|
||||
.projects
|
||||
.get(&project.downgrade())
|
||||
.and_then(|project| project.servers.keys().copied().next());
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
|
||||
let mut this = Self {
|
||||
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
|
||||
project,
|
||||
log_store,
|
||||
current_server_id: None,
|
||||
is_showing_rpc_trace: false,
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
this.show_logs_for_server(server_id, cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
fn editor_for_buffer(
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ViewHandle<Editor> {
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.set_read_only(true);
|
||||
editor.move_to_end(&Default::default(), cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
|
||||
.detach();
|
||||
editor
|
||||
}
|
||||
|
||||
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
||||
let log_store = self.log_store.read(cx);
|
||||
let state = log_store.projects.get(&self.project.downgrade())?;
|
||||
let mut rows = self
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers()
|
||||
.filter_map(|(server_id, language_server_name, worktree_id)| {
|
||||
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let state = state.servers.get(&server_id)?;
|
||||
Some(LogMenuItem {
|
||||
server_id,
|
||||
server_name: language_server_name,
|
||||
worktree,
|
||||
rpc_trace_enabled: state.rpc_state.is_some(),
|
||||
rpc_trace_selected: self.is_showing_rpc_trace
|
||||
&& self.current_server_id == Some(server_id),
|
||||
logs_selected: !self.is_showing_rpc_trace
|
||||
&& self.current_server_id == Some(server_id),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
rows.sort_by_key(|row| row.server_id);
|
||||
rows.dedup_by_key(|row| row.server_id);
|
||||
Some(rows)
|
||||
}
|
||||
|
||||
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
let buffer = self
|
||||
.log_store
|
||||
.read(cx)
|
||||
.log_buffer_for_server(&self.project, server_id);
|
||||
if let Some(buffer) = buffer {
|
||||
self.current_server_id = Some(server_id);
|
||||
self.is_showing_rpc_trace = false;
|
||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
server_id: LanguageServerId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.log_store.update(cx, |log_set, cx| {
|
||||
log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
|
||||
});
|
||||
if let Some(buffer) = buffer {
|
||||
self.current_server_id = Some(server_id);
|
||||
self.is_showing_rpc_trace = true;
|
||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_rpc_trace_for_server(
|
||||
&mut self,
|
||||
server_id: LanguageServerId,
|
||||
enabled: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.log_store.update(cx, |log_store, cx| {
|
||||
if enabled {
|
||||
log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
||||
} else {
|
||||
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
||||
}
|
||||
});
|
||||
if !enabled && Some(server_id) == self.current_server_id {
|
||||
self.show_logs_for_server(server_id, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LspLogView {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspLogView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for LspLogView {
|
||||
fn tab_content<V: View>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_: &AppContext,
|
||||
) -> AnyElement<V> {
|
||||
Label::new("LSP Logs", style.label.clone()).into_any()
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for LspLogView {
|
||||
type Match = <Editor as SearchableItem>::Match;
|
||||
|
||||
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
|
||||
Editor::to_search_event(event)
|
||||
}
|
||||
|
||||
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |e, cx| e.clear_matches(cx))
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.update_matches(matches, cx))
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
self.editor.update(cx, |e, cx| e.query_suggestion(cx))
|
||||
}
|
||||
|
||||
fn activate_match(
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: Vec<Self::Match>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.activate_match(index, matches, cx))
|
||||
}
|
||||
|
||||
fn find_matches(
|
||||
&mut self,
|
||||
query: project::search::SearchQuery,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<Vec<Self::Match>> {
|
||||
self.editor.update(cx, |e, cx| e.find_matches(query, cx))
|
||||
}
|
||||
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
matches: Vec<Self::Match>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<usize> {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.active_match_index(matches, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for LspLogToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
self.menu_open = false;
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(log_view) = item.downcast::<LspLogView>() {
|
||||
self.log_view = Some(log_view.clone());
|
||||
return ToolbarItemLocation::PrimaryLeft {
|
||||
flex: Some((1., false)),
|
||||
};
|
||||
}
|
||||
}
|
||||
self.log_view = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LspLogToolbarItemView {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspLogView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
|
||||
let log_view = log_view.read(cx);
|
||||
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
|
||||
|
||||
let current_server_id = log_view.current_server_id;
|
||||
let current_server = current_server_id.and_then(|current_server_id| {
|
||||
if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) {
|
||||
Some(menu_rows[ix].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
enum Menu {}
|
||||
|
||||
Stack::new()
|
||||
.with_child(Self::render_language_server_menu_header(
|
||||
current_server,
|
||||
&theme,
|
||||
cx,
|
||||
))
|
||||
.with_children(if self.menu_open {
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(menu_rows.into_iter().map(|row| {
|
||||
Self::render_language_server_menu_item(
|
||||
row.server_id,
|
||||
row.server_name,
|
||||
row.worktree,
|
||||
row.rpc_trace_enabled,
|
||||
row.logs_selected,
|
||||
row.rpc_trace_selected,
|
||||
&theme,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.toolbar_dropdown_menu.container)
|
||||
.constrained()
|
||||
.with_width(400.)
|
||||
.with_height(400.)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, |_, this, cx| {
|
||||
this.menu_open = false;
|
||||
cx.notify()
|
||||
}),
|
||||
)
|
||||
.with_hoverable(true)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
const RPC_MESSAGES: &str = "RPC Messages";
|
||||
const SERVER_LOGS: &str = "Server Logs";
|
||||
|
||||
impl LspLogToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
menu_open: false,
|
||||
log_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.menu_open = !self.menu_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_logging_for_server(
|
||||
&mut self,
|
||||
id: LanguageServerId,
|
||||
enabled: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(log_view) = &self.log_view {
|
||||
log_view.update(cx, |log_view, cx| {
|
||||
log_view.toggle_rpc_trace_for_server(id, enabled, cx);
|
||||
if !enabled && Some(id) == log_view.current_server_id {
|
||||
log_view.show_logs_for_server(id, cx);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(log_view) = &self.log_view {
|
||||
log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
|
||||
self.menu_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(log_view) = &self.log_view {
|
||||
log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
|
||||
self.menu_open = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_language_server_menu_header(
|
||||
current_server: Option<LogMenuItem>,
|
||||
theme: &Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ToggleMenu {}
|
||||
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
|
||||
let label: Cow<str> = current_server
|
||||
.and_then(|row| {
|
||||
let worktree = row.worktree.read(cx);
|
||||
Some(
|
||||
format!(
|
||||
"{} ({}) - {}",
|
||||
row.server_name.0,
|
||||
worktree.root_name(),
|
||||
if row.rpc_trace_selected {
|
||||
RPC_MESSAGES
|
||||
} else {
|
||||
SERVER_LOGS
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "No server selected".into());
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.toggle_menu(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_language_server_menu_item(
|
||||
id: LanguageServerId,
|
||||
name: LanguageServerName,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
rpc_trace_enabled: bool,
|
||||
logs_selected: bool,
|
||||
rpc_trace_selected: bool,
|
||||
theme: &Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ActivateLog {}
|
||||
enum ActivateRpcTrace {}
|
||||
|
||||
Flex::column()
|
||||
.with_child({
|
||||
let style = &theme.toolbar_dropdown_menu.section_header;
|
||||
Label::new(
|
||||
format!("{} ({})", name.0, worktree.read(cx).root_name()),
|
||||
style.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.toolbar_dropdown_menu.row_height)
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
|
||||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, logs_selected);
|
||||
Label::new(SERVER_LOGS, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.toolbar_dropdown_menu.row_height)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.show_logs_for_server(id, cx);
|
||||
}),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
|
||||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, rpc_trace_selected);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(RPC_MESSAGES, style.text.clone())
|
||||
.constrained()
|
||||
.with_height(theme.toolbar_dropdown_menu.row_height),
|
||||
)
|
||||
.with_child(
|
||||
ui::checkbox_with_label::<Self, _, Self, _>(
|
||||
Empty::new(),
|
||||
&theme.welcome.checkbox,
|
||||
rpc_trace_enabled,
|
||||
id.0,
|
||||
cx,
|
||||
move |this, enabled, cx| {
|
||||
this.toggle_logging_for_server(id, enabled, cx);
|
||||
},
|
||||
)
|
||||
.flex_float(),
|
||||
)
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.toolbar_dropdown_menu.row_height)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.show_rpc_trace_for_server(id, cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for LogStore {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl Entity for LspLogView {
|
||||
type Event = editor::Event;
|
||||
}
|
||||
|
||||
impl Entity for LspLogToolbarItemView {
|
||||
type Event = ();
|
||||
}
|
||||
99
crates/language_tools/src/lsp_log_tests.rs
Normal file
99
crates/language_tools/src/lsp_log_tests.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::lsp_log::LogMenuItem;
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{serde_json::json, TestAppContext};
|
||||
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_lsp_logs(cx: &mut TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
init_test(cx);
|
||||
|
||||
let mut rust_language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_rust_servers = rust_language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
name: "the-rust-language-server",
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
"test.rs": "",
|
||||
"package.json": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::new(rust_language));
|
||||
});
|
||||
|
||||
let log_store = cx.add_model(|cx| LogStore::new(cx));
|
||||
log_store.update(cx, |store, cx| store.add_project(&project, cx));
|
||||
|
||||
let _rust_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut language_server = fake_rust_servers.next().await.unwrap();
|
||||
language_server
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await;
|
||||
|
||||
let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
|
||||
|
||||
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
|
||||
message: "hello from the server".into(),
|
||||
typ: lsp::MessageType::INFO,
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
log_view.read_with(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
view.menu_items(cx).unwrap(),
|
||||
&[LogMenuItem {
|
||||
server_id: language_server.server.server_id(),
|
||||
server_name: LanguageServerName("the-rust-language-server".into()),
|
||||
worktree: project.read(cx).worktrees(cx).next().unwrap(),
|
||||
rpc_trace_enabled: false,
|
||||
rpc_trace_selected: false,
|
||||
logs_selected: true,
|
||||
}]
|
||||
);
|
||||
assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init_settings(cx);
|
||||
});
|
||||
}
|
||||
675
crates/language_tools/src/syntax_tree_view.rs
Normal file
675
crates/language_tools/src/syntax_tree_view.rs
Normal file
@@ -0,0 +1,675 @@
|
||||
use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{
|
||||
AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
|
||||
ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
|
||||
},
|
||||
fonts::TextStyle,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
|
||||
use std::{mem, ops::Range, sync::Arc};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use tree_sitter::{Node, TreeCursor};
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
actions!(debug, [OpenSyntaxTreeView]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
|
||||
let active_item = workspace.active_item(cx);
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let syntax_tree_view =
|
||||
cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
|
||||
workspace.add_item(Box::new(syntax_tree_view), cx);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub struct SyntaxTreeView {
|
||||
workspace_handle: WeakViewHandle<Workspace>,
|
||||
editor: Option<EditorState>,
|
||||
mouse_y: Option<f32>,
|
||||
line_height: Option<f32>,
|
||||
list_state: UniformListState,
|
||||
selected_descendant_ix: Option<usize>,
|
||||
hovered_descendant_ix: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct SyntaxTreeToolbarItemView {
|
||||
tree_view: Option<ViewHandle<SyntaxTreeView>>,
|
||||
subscription: Option<gpui::Subscription>,
|
||||
menu_open: bool,
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
editor: ViewHandle<Editor>,
|
||||
active_buffer: Option<BufferState>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BufferState {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
excerpt_id: ExcerptId,
|
||||
active_layer: Option<OwnedSyntaxLayerInfo>,
|
||||
}
|
||||
|
||||
impl SyntaxTreeView {
|
||||
pub fn new(
|
||||
workspace_handle: WeakViewHandle<Workspace>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
workspace_handle: workspace_handle.clone(),
|
||||
list_state: UniformListState::default(),
|
||||
editor: None,
|
||||
mouse_y: None,
|
||||
line_height: None,
|
||||
hovered_descendant_ix: None,
|
||||
selected_descendant_ix: None,
|
||||
};
|
||||
|
||||
this.workspace_updated(active_item, cx);
|
||||
cx.observe(
|
||||
&workspace_handle.upgrade(cx).unwrap(),
|
||||
|this, workspace, cx| {
|
||||
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn workspace_updated(
|
||||
&mut self,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(item) = active_item {
|
||||
if item.id() != cx.view_id() {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
self.set_editor(editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(state) = &self.editor {
|
||||
if state.editor == editor {
|
||||
return;
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx)
|
||||
});
|
||||
}
|
||||
|
||||
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
|
||||
let did_reparse = match event {
|
||||
editor::Event::Reparsed => true,
|
||||
editor::Event::SelectionsChanged { .. } => false,
|
||||
_ => return,
|
||||
};
|
||||
this.editor_updated(did_reparse, cx);
|
||||
});
|
||||
|
||||
self.editor = Some(EditorState {
|
||||
editor,
|
||||
_subscription: subscription,
|
||||
active_buffer: None,
|
||||
});
|
||||
self.editor_updated(true, cx);
|
||||
}
|
||||
|
||||
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
|
||||
let editor_state = self.editor.as_mut()?;
|
||||
let editor = &editor_state.editor.read(cx);
|
||||
let selection_range = editor.selections.last::<usize>(cx).range();
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let (buffer, range, excerpt_id) = multibuffer
|
||||
.range_to_buffer_ranges(selection_range, cx)
|
||||
.pop()?;
|
||||
|
||||
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
|
||||
// from that buffer.
|
||||
let buffer_state = editor_state
|
||||
.active_buffer
|
||||
.get_or_insert_with(|| BufferState {
|
||||
buffer: buffer.clone(),
|
||||
excerpt_id,
|
||||
active_layer: None,
|
||||
});
|
||||
let mut prev_layer = None;
|
||||
if did_reparse {
|
||||
prev_layer = buffer_state.active_layer.take();
|
||||
}
|
||||
if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
|
||||
buffer_state.buffer = buffer.clone();
|
||||
buffer_state.excerpt_id = excerpt_id;
|
||||
buffer_state.active_layer = None;
|
||||
}
|
||||
|
||||
let layer = match &mut buffer_state.active_layer {
|
||||
Some(layer) => layer,
|
||||
None => {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let layer = if let Some(prev_layer) = prev_layer {
|
||||
let prev_range = prev_layer.node().byte_range();
|
||||
snapshot
|
||||
.syntax_layers()
|
||||
.filter(|layer| layer.language == &prev_layer.language)
|
||||
.min_by_key(|layer| {
|
||||
let range = layer.node().byte_range();
|
||||
((range.start as i64) - (prev_range.start as i64)).abs()
|
||||
+ ((range.end as i64) - (prev_range.end as i64)).abs()
|
||||
})?
|
||||
} else {
|
||||
snapshot.syntax_layers().next()?
|
||||
};
|
||||
buffer_state.active_layer.insert(layer.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
// Within the active layer, find the syntax node under the cursor,
|
||||
// and scroll to it.
|
||||
let mut cursor = layer.node().walk();
|
||||
while cursor.goto_first_child_for_byte(range.start).is_some() {
|
||||
if !range.is_empty() && cursor.node().end_byte() == range.start {
|
||||
cursor.goto_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
// Ascend to the smallest ancestor that contains the range.
|
||||
loop {
|
||||
let node_range = cursor.node().byte_range();
|
||||
if node_range.start <= range.start && node_range.end >= range.end {
|
||||
break;
|
||||
}
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let descendant_ix = cursor.descendant_index();
|
||||
self.selected_descendant_ix = Some(descendant_ix);
|
||||
self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
|
||||
|
||||
cx.notify();
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
|
||||
let line_height = self.line_height?;
|
||||
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
|
||||
|
||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
|
||||
// Put the cursor at the beginning of the node.
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
});
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
|
||||
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
|
||||
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
|
||||
if self.hovered_descendant_ix != Some(ix) {
|
||||
self.hovered_descendant_ix = Some(ix);
|
||||
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
|
||||
editor.clear_background_highlights::<Self>(cx);
|
||||
editor.highlight_background::<Self>(
|
||||
vec![range],
|
||||
|theme| theme.editor.document_highlight_write_background,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_editor_with_range_for_descendant_ix(
|
||||
&self,
|
||||
descendant_ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
|
||||
) -> Option<()> {
|
||||
let editor_state = self.editor.as_ref()?;
|
||||
let buffer_state = editor_state.active_buffer.as_ref()?;
|
||||
let layer = buffer_state.active_layer.as_ref()?;
|
||||
|
||||
// Find the node.
|
||||
let mut cursor = layer.node().walk();
|
||||
cursor.goto_descendant(descendant_ix);
|
||||
let node = cursor.node();
|
||||
let range = node.byte_range();
|
||||
|
||||
// Build a text anchor range.
|
||||
let buffer = buffer_state.buffer.read(cx);
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|
||||
|
||||
// Build a multibuffer anchor range.
|
||||
let multibuffer = editor_state.editor.read(cx).buffer();
|
||||
let multibuffer = multibuffer.read(cx).snapshot(cx);
|
||||
let excerpt_id = buffer_state.excerpt_id;
|
||||
let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
|
||||
..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
|
||||
|
||||
// Update the editor with the anchor range.
|
||||
editor_state.editor.update(cx, |editor, cx| {
|
||||
f(editor, range, cx);
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn render_node(
|
||||
cursor: &TreeCursor,
|
||||
depth: u32,
|
||||
selected: bool,
|
||||
hovered: bool,
|
||||
list_hovered: bool,
|
||||
style: &TextStyle,
|
||||
editor_theme: &theme::Editor,
|
||||
cx: &AppContext,
|
||||
) -> gpui::AnyElement<SyntaxTreeView> {
|
||||
let node = cursor.node();
|
||||
let mut range_style = style.clone();
|
||||
let em_width = style.em_width(cx.font_cache());
|
||||
let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
|
||||
|
||||
range_style.color = editor_theme.line_number;
|
||||
|
||||
let mut anonymous_node_style = style.clone();
|
||||
let string_color = editor_theme
|
||||
.syntax
|
||||
.highlights
|
||||
.iter()
|
||||
.find_map(|(name, style)| (name == "string").then(|| style.color)?);
|
||||
let property_color = editor_theme
|
||||
.syntax
|
||||
.highlights
|
||||
.iter()
|
||||
.find_map(|(name, style)| (name == "property").then(|| style.color)?);
|
||||
if let Some(color) = string_color {
|
||||
anonymous_node_style.color = color;
|
||||
}
|
||||
|
||||
let mut row = Flex::row();
|
||||
if let Some(field_name) = cursor.field_name() {
|
||||
let mut field_style = style.clone();
|
||||
if let Some(color) = property_color {
|
||||
field_style.color = color;
|
||||
}
|
||||
|
||||
row.add_children([
|
||||
Label::new(field_name, field_style),
|
||||
Label::new(": ", style.clone()),
|
||||
]);
|
||||
}
|
||||
|
||||
return row
|
||||
.with_child(
|
||||
if node.is_named() {
|
||||
Label::new(node.kind(), style.clone())
|
||||
} else {
|
||||
Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
|
||||
}
|
||||
.contained()
|
||||
.with_margin_right(em_width),
|
||||
)
|
||||
.with_child(Label::new(format_node_range(node), range_style))
|
||||
.contained()
|
||||
.with_background_color(if selected {
|
||||
editor_theme.selection.selection
|
||||
} else if hovered && list_hovered {
|
||||
editor_theme.active_line_background
|
||||
} else {
|
||||
Default::default()
|
||||
})
|
||||
.with_padding_left(gutter_padding + depth as f32 * 18.0)
|
||||
.into_any();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SyntaxTreeView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SyntaxTreeView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SyntaxTreeView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
||||
let settings = settings::get::<ThemeSettings>(cx);
|
||||
let font_family_id = settings.buffer_font_family;
|
||||
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
|
||||
let font_properties = Default::default();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(font_family_id, &font_properties)
|
||||
.unwrap();
|
||||
let font_size = settings.buffer_font_size(cx);
|
||||
|
||||
let editor_theme = settings.theme.editor.clone();
|
||||
let style = TextStyle {
|
||||
color: editor_theme.text_color,
|
||||
font_family_name,
|
||||
font_family_id,
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties: Default::default(),
|
||||
underline: Default::default(),
|
||||
};
|
||||
|
||||
let line_height = cx.font_cache().line_height(font_size);
|
||||
if Some(line_height) != self.line_height {
|
||||
self.line_height = Some(line_height);
|
||||
self.hover_state_changed(cx);
|
||||
}
|
||||
|
||||
if let Some(layer) = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.active_buffer.as_ref())
|
||||
.and_then(|buffer| buffer.active_layer.as_ref())
|
||||
{
|
||||
let layer = layer.clone();
|
||||
let theme = editor_theme.clone();
|
||||
return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
|
||||
let list_hovered = state.hovered();
|
||||
UniformList::new(
|
||||
self.list_state.clone(),
|
||||
layer.node().descendant_count(),
|
||||
cx,
|
||||
move |this, range, items, cx| {
|
||||
let mut cursor = layer.node().walk();
|
||||
let mut descendant_ix = range.start as usize;
|
||||
cursor.goto_descendant(descendant_ix);
|
||||
let mut depth = cursor.depth();
|
||||
let mut visited_children = false;
|
||||
while descendant_ix < range.end {
|
||||
if visited_children {
|
||||
if cursor.goto_next_sibling() {
|
||||
visited_children = false;
|
||||
} else if cursor.goto_parent() {
|
||||
depth -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
items.push(Self::render_node(
|
||||
&cursor,
|
||||
depth,
|
||||
Some(descendant_ix) == this.selected_descendant_ix,
|
||||
Some(descendant_ix) == this.hovered_descendant_ix,
|
||||
list_hovered,
|
||||
&style,
|
||||
&theme,
|
||||
cx,
|
||||
));
|
||||
descendant_ix += 1;
|
||||
if cursor.goto_first_child() {
|
||||
depth += 1;
|
||||
} else {
|
||||
visited_children = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.on_move(move |event, this, cx| {
|
||||
let y = event.position.y() - event.region.origin_y();
|
||||
this.mouse_y = Some(y);
|
||||
this.hover_state_changed(cx);
|
||||
})
|
||||
.on_click(MouseButton::Left, move |event, this, cx| {
|
||||
let y = event.position.y() - event.region.origin_y();
|
||||
this.handle_click(y, cx);
|
||||
})
|
||||
.contained()
|
||||
.with_background_color(editor_theme.background)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for SyntaxTreeView {
|
||||
fn tab_content<V: View>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_: &AppContext,
|
||||
) -> gpui::AnyElement<V> {
|
||||
Label::new("Syntax Tree", style.label.clone()).into_any()
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
|
||||
if let Some(editor) = &self.editor {
|
||||
clone.set_editor(editor.editor.clone(), cx)
|
||||
}
|
||||
Some(clone)
|
||||
}
|
||||
}
|
||||
|
||||
impl SyntaxTreeToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
menu_open: false,
|
||||
tree_view: None,
|
||||
subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_menu(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<'_, '_, Self>,
|
||||
) -> Option<gpui::AnyElement<Self>> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let tree_view = self.tree_view.as_ref()?;
|
||||
let tree_view = tree_view.read(cx);
|
||||
|
||||
let editor_state = tree_view.editor.as_ref()?;
|
||||
let buffer_state = editor_state.active_buffer.as_ref()?;
|
||||
let active_layer = buffer_state.active_layer.clone()?;
|
||||
let active_buffer = buffer_state.buffer.read(cx).snapshot();
|
||||
|
||||
enum Menu {}
|
||||
|
||||
Some(
|
||||
Stack::new()
|
||||
.with_child(Self::render_header(&theme, &active_layer, cx))
|
||||
.with_children(self.menu_open.then(|| {
|
||||
Overlay::new(
|
||||
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(active_buffer.syntax_layers().enumerate().map(
|
||||
|(ix, layer)| {
|
||||
Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.toolbar_dropdown_menu.container)
|
||||
.constrained()
|
||||
.with_width(400.)
|
||||
.with_height(400.)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, |_, this, cx| {
|
||||
this.menu_open = false;
|
||||
cx.notify()
|
||||
}),
|
||||
)
|
||||
.with_hoverable(true)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.left()
|
||||
}))
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.menu_open = !self.menu_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
let tree_view = self.tree_view.as_ref()?;
|
||||
tree_view.update(cx, |view, cx| {
|
||||
let editor_state = view.editor.as_mut()?;
|
||||
let buffer_state = editor_state.active_buffer.as_mut()?;
|
||||
let snapshot = buffer_state.buffer.read(cx).snapshot();
|
||||
let layer = snapshot.syntax_layers().nth(layer_ix)?;
|
||||
buffer_state.active_layer = Some(layer.to_owned());
|
||||
view.selected_descendant_ix = None;
|
||||
self.menu_open = false;
|
||||
cx.notify();
|
||||
Some(())
|
||||
})
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
theme: &Arc<Theme>,
|
||||
active_layer: &OwnedSyntaxLayerInfo,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ToggleMenu {}
|
||||
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(active_layer.language.name().to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_margin_right(style.secondary_text_spacing),
|
||||
)
|
||||
.with_child(Label::new(
|
||||
format_node_range(active_layer.node()),
|
||||
style
|
||||
.secondary_text
|
||||
.clone()
|
||||
.unwrap_or_else(|| style.text.clone()),
|
||||
))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.toggle_menu(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_menu_item(
|
||||
theme: &Arc<Theme>,
|
||||
active_layer: &OwnedSyntaxLayerInfo,
|
||||
layer: SyntaxLayerInfo,
|
||||
layer_ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ActivateLayer {}
|
||||
MouseEventHandler::<ActivateLayer, _>::new(layer_ix, cx, move |state, _| {
|
||||
let is_selected = layer.node() == active_layer.node();
|
||||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, is_selected);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(layer.language.name().to_string(), style.text.clone())
|
||||
.contained()
|
||||
.with_margin_right(style.secondary_text_spacing),
|
||||
)
|
||||
.with_child(Label::new(
|
||||
format_node_range(layer.node()),
|
||||
style
|
||||
.secondary_text
|
||||
.clone()
|
||||
.unwrap_or_else(|| style.text.clone()),
|
||||
))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.select_layer(layer_ix, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn format_node_range(node: Node) -> String {
|
||||
let start = node.start_position();
|
||||
let end = node.end_position();
|
||||
format!(
|
||||
"[{}:{} - {}:{}]",
|
||||
start.row + 1,
|
||||
start.column + 1,
|
||||
end.row + 1,
|
||||
end.column + 1,
|
||||
)
|
||||
}
|
||||
|
||||
impl Entity for SyntaxTreeToolbarItemView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SyntaxTreeToolbarItemView {
|
||||
fn ui_name() -> &'static str {
|
||||
"SyntaxTreeToolbarItemView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
|
||||
self.render_menu(cx)
|
||||
.unwrap_or_else(|| Empty::new().into_any())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for SyntaxTreeToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
self.menu_open = false;
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(view) = item.downcast::<SyntaxTreeView>() {
|
||||
self.tree_view = Some(view.clone());
|
||||
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
|
||||
return ToolbarItemLocation::PrimaryLeft {
|
||||
flex: Some((1., false)),
|
||||
};
|
||||
}
|
||||
}
|
||||
self.tree_view = None;
|
||||
self.subscription = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
@@ -260,9 +260,10 @@ impl LanguageServer {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
let header = std::str::from_utf8(&buffer)?;
|
||||
let message_len: usize = header
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
@@ -301,7 +302,7 @@ impl LanguageServer {
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize message:\n{}",
|
||||
"failed to deserialize LSP message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
@@ -747,6 +748,15 @@ impl fmt::Display for LanguageServerId {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LanguageServer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("LanguageServer")
|
||||
.field("id", &self.server_id.0)
|
||||
.field("name", &self.name)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
@@ -782,6 +792,8 @@ impl LanguageServer {
|
||||
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
document_range_formatting_provider: Some(OneOf::Left(true)),
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
use collections::{hash_map, HashMap};
|
||||
use editor::Editor;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{
|
||||
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
|
||||
ParentElement, Stack,
|
||||
},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
|
||||
ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageServerId, LanguageServerName};
|
||||
use project::{Project, WorktreeId};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use theme::{ui, Theme};
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
const SEND_LINE: &str = "// Send:\n";
|
||||
const RECEIVE_LINE: &str = "// Receive:\n";
|
||||
|
||||
struct LogStore {
|
||||
projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
|
||||
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
|
||||
}
|
||||
|
||||
struct LogStoreProject {
|
||||
servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
struct LogStoreLanguageServer {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
_subscription: lsp::Subscription,
|
||||
}
|
||||
|
||||
pub struct LspLogView {
|
||||
log_store: ModelHandle<LogStore>,
|
||||
current_server_id: Option<LanguageServerId>,
|
||||
editor: Option<ViewHandle<Editor>>,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
pub struct LspLogToolbarItemView {
|
||||
log_view: Option<ViewHandle<LspLogView>>,
|
||||
menu_open: bool,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum MessageKind {
|
||||
Send,
|
||||
Receive,
|
||||
}
|
||||
|
||||
actions!(log, [OpenLanguageServerLogs]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let log_set = cx.add_model(|cx| LogStore::new(cx));
|
||||
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() {
|
||||
workspace.add_item(
|
||||
Box::new(cx.add_view(|cx| {
|
||||
LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
fn new(cx: &mut ModelContext<Self>) -> Self {
|
||||
let (io_tx, mut io_rx) = mpsc::unbounded();
|
||||
let this = Self {
|
||||
projects: HashMap::default(),
|
||||
io_tx,
|
||||
};
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
message.push('\n');
|
||||
this.on_io(project, server_id, is_output, &message, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
pub fn has_enabled_logs_for_language_server(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
) -> bool {
|
||||
self.projects
|
||||
.get(&project.downgrade())
|
||||
.map_or(false, |store| store.servers.contains_key(&server_id))
|
||||
}
|
||||
|
||||
pub fn enable_logs_for_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
let server = project.read(cx).language_server_for_id(server_id)?;
|
||||
let weak_project = project.downgrade();
|
||||
let project_logs = match self.projects.entry(weak_project) {
|
||||
hash_map::Entry::Occupied(entry) => entry.into_mut(),
|
||||
hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject {
|
||||
servers: HashMap::default(),
|
||||
_subscription: cx.observe_release(&project, move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
}),
|
||||
}),
|
||||
};
|
||||
let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
|
||||
let io_tx = self.io_tx.clone();
|
||||
let language = project.read(cx).languages().language_for_name("JSON");
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
|
||||
cx.spawn_weak({
|
||||
let buffer = buffer.clone();
|
||||
|_, mut cx| async move {
|
||||
let language = language.await.ok();
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let project = project.downgrade();
|
||||
LogStoreLanguageServer {
|
||||
buffer,
|
||||
last_message_kind: None,
|
||||
_subscription: server.on_io(move |is_received, json| {
|
||||
io_tx
|
||||
.unbounded_send((project, server_id, is_received, json.to_string()))
|
||||
.ok();
|
||||
}),
|
||||
}
|
||||
});
|
||||
Some(server_log_state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn disable_logs_for_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
_: &mut ModelContext<Self>,
|
||||
) {
|
||||
let project = project.downgrade();
|
||||
if let Some(store) = self.projects.get_mut(&project) {
|
||||
store.servers.remove(&server_id);
|
||||
if store.servers.is_empty() {
|
||||
self.projects.remove(&project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_io(
|
||||
&mut self,
|
||||
project: WeakModelHandle<Project>,
|
||||
language_server_id: LanguageServerId,
|
||||
is_received: bool,
|
||||
message: &str,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let state = self
|
||||
.projects
|
||||
.get_mut(&project)?
|
||||
.servers
|
||||
.get_mut(&language_server_id)?;
|
||||
state.buffer.update(cx, |buffer, cx| {
|
||||
let kind = if is_received {
|
||||
MessageKind::Receive
|
||||
} else {
|
||||
MessageKind::Send
|
||||
};
|
||||
if state.last_message_kind != Some(kind) {
|
||||
let len = buffer.len();
|
||||
let line = match kind {
|
||||
MessageKind::Send => SEND_LINE,
|
||||
MessageKind::Receive => RECEIVE_LINE,
|
||||
};
|
||||
buffer.edit([(len..len, line)], None, cx);
|
||||
state.last_message_kind = Some(kind);
|
||||
}
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, message)], None, cx);
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LspLogView {
|
||||
fn new(
|
||||
project: ModelHandle<Project>,
|
||||
log_set: ModelHandle<LogStore>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
log_store: log_set,
|
||||
editor: None,
|
||||
current_server_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
let buffer = self.log_store.update(cx, |log_set, cx| {
|
||||
log_set.enable_logs_for_language_server(&self.project, server_id, cx)
|
||||
});
|
||||
if let Some(buffer) = buffer {
|
||||
self.current_server_id = Some(server_id);
|
||||
self.editor = Some(cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
|
||||
editor.set_read_only(true);
|
||||
editor.move_to_end(&Default::default(), cx);
|
||||
editor
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_logging_for_server(
|
||||
&mut self,
|
||||
server_id: LanguageServerId,
|
||||
enabled: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.log_store.update(cx, |log_store, cx| {
|
||||
if enabled {
|
||||
log_store.enable_logs_for_language_server(&self.project, server_id, cx);
|
||||
} else {
|
||||
log_store.disable_logs_for_language_server(&self.project, server_id, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LspLogView {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspLogView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(editor) = &self.editor {
|
||||
ChildView::new(&editor, cx).into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for LspLogView {
|
||||
fn tab_content<V: View>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_: &AppContext,
|
||||
) -> AnyElement<V> {
|
||||
Label::new("LSP Logs", style.label.clone()).into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for LspLogToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
self.menu_open = false;
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(log_view) = item.downcast::<LspLogView>() {
|
||||
self.log_view = Some(log_view.clone());
|
||||
return ToolbarItemLocation::PrimaryLeft {
|
||||
flex: Some((1., false)),
|
||||
};
|
||||
}
|
||||
}
|
||||
self.log_view = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LspLogToolbarItemView {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspLogView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
|
||||
let project = self.project.read(cx);
|
||||
let log_view = log_view.read(cx);
|
||||
let log_store = log_view.log_store.read(cx);
|
||||
|
||||
let mut language_servers = project
|
||||
.language_servers()
|
||||
.map(|(id, name, worktree)| {
|
||||
(
|
||||
id,
|
||||
name,
|
||||
worktree,
|
||||
log_store.has_enabled_logs_for_language_server(&self.project, id),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
language_servers.sort_by_key(|a| (a.0, a.2));
|
||||
language_servers.dedup_by_key(|a| a.0);
|
||||
|
||||
let current_server_id = log_view.current_server_id;
|
||||
let current_server = current_server_id.and_then(|current_server_id| {
|
||||
if let Ok(ix) = language_servers.binary_search_by_key(¤t_server_id, |e| e.0) {
|
||||
Some(language_servers[ix].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
enum Menu {}
|
||||
|
||||
Stack::new()
|
||||
.with_child(Self::render_language_server_menu_header(
|
||||
current_server,
|
||||
&self.project,
|
||||
&theme,
|
||||
cx,
|
||||
))
|
||||
.with_children(if self.menu_open {
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(language_servers.into_iter().filter_map(
|
||||
|(id, name, worktree_id, logging_enabled)| {
|
||||
Self::render_language_server_menu_item(
|
||||
id,
|
||||
name,
|
||||
worktree_id,
|
||||
logging_enabled,
|
||||
Some(id) == current_server_id,
|
||||
&self.project,
|
||||
&theme,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.context_menu.container)
|
||||
.constrained()
|
||||
.with_width(400.)
|
||||
.with_height(400.)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, |_, this, cx| {
|
||||
this.menu_open = false;
|
||||
cx.notify()
|
||||
}),
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl LspLogToolbarItemView {
|
||||
pub fn new(project: ModelHandle<Project>) -> Self {
|
||||
Self {
|
||||
menu_open: false,
|
||||
log_view: None,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.menu_open = !self.menu_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_logging_for_server(
|
||||
&mut self,
|
||||
id: LanguageServerId,
|
||||
enabled: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(log_view) = &self.log_view {
|
||||
log_view.update(cx, |log_view, cx| {
|
||||
log_view.toggle_logging_for_server(id, enabled, cx);
|
||||
if !enabled && Some(id) == log_view.current_server_id {
|
||||
log_view.current_server_id = None;
|
||||
log_view.editor = None;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(log_view) = &self.log_view {
|
||||
log_view.update(cx, |log_view, cx| {
|
||||
log_view.show_logs_for_server(id, cx);
|
||||
});
|
||||
self.menu_open = false;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_language_server_menu_header(
|
||||
current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ToggleMenu {}
|
||||
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
|
||||
let project = project.read(cx);
|
||||
let label: Cow<str> = current_server
|
||||
.and_then(|(_, server_name, worktree_id, _)| {
|
||||
let worktree = project.worktree_for_id(worktree_id, cx)?;
|
||||
let worktree = &worktree.read(cx);
|
||||
Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
|
||||
})
|
||||
.unwrap_or_else(|| "No server selected".into());
|
||||
Label::new(
|
||||
label,
|
||||
theme
|
||||
.context_menu
|
||||
.item
|
||||
.style_for(state, false)
|
||||
.label
|
||||
.clone(),
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.toggle_menu(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn render_language_server_menu_item(
|
||||
id: LanguageServerId,
|
||||
name: LanguageServerName,
|
||||
worktree_id: WorktreeId,
|
||||
logging_enabled: bool,
|
||||
is_selected: bool,
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<impl Element<Self>> {
|
||||
enum ActivateLog {}
|
||||
let project = project.read(cx);
|
||||
let worktree = project.worktree_for_id(worktree_id, cx)?;
|
||||
let worktree = &worktree.read(cx);
|
||||
if !worktree.is_visible() {
|
||||
return None;
|
||||
}
|
||||
let label = format!("{} - ({})", name.0, worktree.root_name());
|
||||
|
||||
Some(
|
||||
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
|
||||
let item_style = theme.context_menu.item.style_for(state, is_selected);
|
||||
Flex::row()
|
||||
.with_child(ui::checkbox_with_label::<Self, _, Self, _>(
|
||||
Empty::new(),
|
||||
&theme.welcome.checkbox,
|
||||
logging_enabled,
|
||||
id.0,
|
||||
cx,
|
||||
move |this, enabled, cx| {
|
||||
this.toggle_logging_for_server(id, enabled, cx);
|
||||
},
|
||||
))
|
||||
.with_child(Label::new(label, item_style.label.clone()).aligned().left())
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, view, cx| {
|
||||
view.show_logs_for_server(id, cx);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for LogStore {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl Entity for LspLogView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl Entity for LspLogToolbarItemView {
|
||||
type Event = ();
|
||||
}
|
||||
@@ -18,4 +18,4 @@ metal = "0.21.0"
|
||||
objc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
||||
bindgen = "0.65.1"
|
||||
|
||||
@@ -11,7 +11,7 @@ use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Ty
|
||||
/// "Hello from Wasm".into()
|
||||
/// }
|
||||
/// ```
|
||||
/// This macro makes a function defined guest-side avaliable host-side.
|
||||
/// This macro makes a function defined guest-side available host-side.
|
||||
/// Note that all arguments and return types must be `serde`.
|
||||
#[proc_macro_attribute]
|
||||
pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
@@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
/// #[import]
|
||||
/// pub fn operating_system_name() -> String;
|
||||
/// ```
|
||||
/// This macro makes a function defined host-side avaliable guest-side.
|
||||
/// This macro makes a function defined host-side available guest-side.
|
||||
/// Note that all arguments and return types must be `serde`.
|
||||
/// All that's provided is a signature, as the function is implemented host-side.
|
||||
#[proc_macro_attribute]
|
||||
|
||||
@@ -127,7 +127,7 @@ use plugin_handles::RopeHandle;
|
||||
pub fn append(rope: RopeHandle, string: &str);
|
||||
```
|
||||
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle.
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle.
|
||||
|
||||
To illustrate that point, here's an example. First, we'd define a plugin-side function as follows:
|
||||
|
||||
@@ -177,7 +177,7 @@ So here's what calling `append_newline` would do, from the top:
|
||||
|
||||
6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource.
|
||||
|
||||
Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
|
||||
Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
|
||||
|
||||
## Final Notes
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ impl PluginBuilder {
|
||||
"env",
|
||||
&format!("__{}", name),
|
||||
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
|
||||
// TODO: use try block once avaliable
|
||||
// TODO: use try block once available
|
||||
let result: Result<(WasiBuffer, Memory, _), Trap> = (|| {
|
||||
// grab a handle to the memory
|
||||
let plugin_memory = match caller.get_export("memory") {
|
||||
@@ -211,7 +211,7 @@ impl PluginBuilder {
|
||||
"env",
|
||||
&format!("__{}", name),
|
||||
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
|
||||
// TODO: use try block once avaliable
|
||||
// TODO: use try block once available
|
||||
let result: Result<(WasiBuffer, Memory, Vec<u8>), Trap> = (|| {
|
||||
// grab a handle to the memory
|
||||
let plugin_memory = match caller.get_export("memory") {
|
||||
@@ -297,7 +297,7 @@ pub enum PluginBinary<'a> {
|
||||
Precompiled(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface.
|
||||
/// Represents a WebAssembly plugin, with access to the WebAssembly System Interface.
|
||||
/// Build a new plugin using [`PluginBuilder`].
|
||||
pub struct Plugin {
|
||||
store: Store<WasiCtxAlloc>,
|
||||
@@ -559,7 +559,7 @@ impl Plugin {
|
||||
.ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?;
|
||||
|
||||
// write the argument to linear memory
|
||||
// this returns a (ptr, lentgh) pair
|
||||
// this returns a (ptr, length) pair
|
||||
let arg_buffer = Self::bytes_to_buffer(
|
||||
self.store.data().alloc_buffer(),
|
||||
&mut plugin_memory,
|
||||
@@ -569,7 +569,7 @@ impl Plugin {
|
||||
.await?;
|
||||
|
||||
// call the function, passing in the buffer and its length
|
||||
// this returns a ptr to a (ptr, lentgh) pair
|
||||
// this returns a ptr to a (ptr, length) pair
|
||||
let result_buffer = handle
|
||||
.function
|
||||
.call_async(&mut self.store, arg_buffer.into_u64())
|
||||
|
||||
@@ -487,6 +487,14 @@ impl LspCommand for GetTypeDefinition {
|
||||
type LspRequest = lsp::request::GotoTypeDefinition;
|
||||
type ProtoRequest = proto::GetTypeDefinition;
|
||||
|
||||
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
|
||||
match &capabilities.type_definition_provider {
|
||||
None => false,
|
||||
Some(lsp::TypeDefinitionProviderCapability::Simple(false)) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -1111,14 +1119,18 @@ impl LspCommand for GetHover {
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
Ok(message.and_then(|hover| {
|
||||
let range = hover.range.map(|range| {
|
||||
cx.read(|cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let token_start =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||
let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
||||
})
|
||||
let (language, range) = cx.read(|cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
(
|
||||
buffer.language().cloned(),
|
||||
hover.range.map(|range| {
|
||||
let token_start =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||
let token_end =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
fn hover_blocks_from_marked_string(
|
||||
@@ -1163,7 +1175,11 @@ impl LspCommand for GetHover {
|
||||
}],
|
||||
});
|
||||
|
||||
Some(Hover { contents, range })
|
||||
Some(Hover {
|
||||
contents,
|
||||
range,
|
||||
language,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1247,16 +1263,9 @@ impl LspCommand for GetHover {
|
||||
self,
|
||||
message: proto::GetHoverResponse,
|
||||
_: ModelHandle<Project>,
|
||||
_: ModelHandle<Buffer>,
|
||||
_: AsyncAppContext,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
||||
language::proto::deserialize_anchor(start)
|
||||
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let contents: Vec<_> = message
|
||||
.contents
|
||||
.into_iter()
|
||||
@@ -1271,12 +1280,23 @@ impl LspCommand for GetHover {
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
if contents.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(if contents.is_empty() {
|
||||
None
|
||||
let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
|
||||
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
||||
language::proto::deserialize_anchor(start)
|
||||
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
||||
} else {
|
||||
Some(Hover { contents, range })
|
||||
})
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Some(Hover {
|
||||
contents,
|
||||
range,
|
||||
language,
|
||||
}))
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
|
||||
@@ -1499,7 +1519,11 @@ impl LspCommand for GetCodeActions {
|
||||
type ProtoRequest = proto::GetCodeActions;
|
||||
|
||||
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
|
||||
capabilities.code_action_provider.is_some()
|
||||
match &capabilities.code_action_provider {
|
||||
None => false,
|
||||
Some(lsp::CodeActionProviderCapability::Simple(false)) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
@@ -1717,8 +1741,7 @@ impl LspCommand for OnTypeFormatting {
|
||||
.await?;
|
||||
|
||||
let tab_size = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
language_settings(buffer.language(), buffer.file(), cx).tab_size
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -7,6 +7,8 @@ pub mod worktree;
|
||||
|
||||
#[cfg(test)]
|
||||
mod project_tests;
|
||||
#[cfg(test)]
|
||||
mod worktree_tests;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client, TypedEnvelope, UserStore};
|
||||
@@ -28,7 +30,7 @@ use gpui::{
|
||||
ModelHandle, Task, WeakModelHandle,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
|
||||
language_settings::{language_settings, FormatOnSave, Formatter},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
@@ -37,8 +39,8 @@ use language::{
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
|
||||
Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
|
||||
Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
|
||||
PendingLanguageServer, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
|
||||
Transaction, Unclipped,
|
||||
PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
|
||||
Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
@@ -69,10 +71,13 @@ use std::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminals::Terminals;
|
||||
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
|
||||
use util::{
|
||||
debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
|
||||
ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
pub use worktree::*;
|
||||
@@ -242,10 +247,11 @@ pub struct Collaborator {
|
||||
pub replica_id: ReplicaId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
LanguageServerAdded(LanguageServerId),
|
||||
LanguageServerRemoved(LanguageServerId),
|
||||
LanguageServerLog(LanguageServerId, String),
|
||||
ActiveEntryChanged(Option<ProjectEntryId>),
|
||||
WorktreeAdded,
|
||||
WorktreeRemoved(WorktreeId),
|
||||
@@ -356,6 +362,7 @@ pub enum HoverBlockKind {
|
||||
pub struct Hover {
|
||||
pub contents: Vec<HoverBlock>,
|
||||
pub range: Option<Range<language::Anchor>>,
|
||||
pub language: Option<Arc<Language>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -460,6 +467,7 @@ impl Project {
|
||||
client.add_model_request_handler(Self::handle_update_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
|
||||
client.add_model_message_handler(Self::handle_update_worktree);
|
||||
client.add_model_message_handler(Self::handle_update_worktree_settings);
|
||||
client.add_model_request_handler(Self::handle_create_project_entry);
|
||||
client.add_model_request_handler(Self::handle_rename_project_entry);
|
||||
client.add_model_request_handler(Self::handle_copy_project_entry);
|
||||
@@ -519,7 +527,7 @@ impl Project {
|
||||
_subscriptions: vec![
|
||||
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
|
||||
],
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
||||
active_entry: None,
|
||||
languages,
|
||||
@@ -588,7 +596,7 @@ impl Project {
|
||||
active_entry: None,
|
||||
collaborators: Default::default(),
|
||||
join_project_response_message_id: response.message_id,
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
||||
languages,
|
||||
user_store: user_store.clone(),
|
||||
@@ -686,42 +694,37 @@ impl Project {
|
||||
}
|
||||
|
||||
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let settings = all_language_settings(cx);
|
||||
|
||||
let mut language_servers_to_start = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
|
||||
{
|
||||
if settings
|
||||
.language(Some(&language.name()))
|
||||
.enable_language_server
|
||||
{
|
||||
let worktree = file.worktree.read(cx);
|
||||
language_servers_to_start.push((
|
||||
worktree.id(),
|
||||
worktree.as_local().unwrap().abs_path().clone(),
|
||||
language.clone(),
|
||||
));
|
||||
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
|
||||
let settings = language_settings(Some(language), Some(file), cx);
|
||||
if settings.enable_language_server {
|
||||
if let Some(file) = File::from_dyn(Some(file)) {
|
||||
language_servers_to_start
|
||||
.push((file.worktree.clone(), language.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut language_servers_to_stop = Vec::new();
|
||||
for language in self.languages.to_vec() {
|
||||
for lsp_adapter in language.lsp_adapters() {
|
||||
if !settings
|
||||
.language(Some(&language.name()))
|
||||
.enable_language_server
|
||||
{
|
||||
let lsp_name = &lsp_adapter.name;
|
||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||
if lsp_name == started_lsp_name {
|
||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||
}
|
||||
}
|
||||
let languages = self.languages.to_vec();
|
||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||
let language = languages.iter().find(|l| {
|
||||
l.lsp_adapters()
|
||||
.iter()
|
||||
.any(|adapter| &adapter.name == started_lsp_name)
|
||||
});
|
||||
if let Some(language) = language {
|
||||
let worktree = self.worktree_for_id(*worktree_id, cx);
|
||||
let file = worktree.and_then(|tree| {
|
||||
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
|
||||
});
|
||||
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
|
||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,8 +736,9 @@ impl Project {
|
||||
}
|
||||
|
||||
// Start all the newly-enabled language servers.
|
||||
for (worktree_id, worktree_path, language) in language_servers_to_start {
|
||||
self.start_language_servers(worktree_id, worktree_path, language, cx);
|
||||
for (worktree, language) in language_servers_to_start {
|
||||
let worktree_path = worktree.read(cx).abs_path();
|
||||
self.start_language_servers(&worktree, worktree_path, language, cx);
|
||||
}
|
||||
|
||||
if !self.copilot_enabled && Copilot::global(cx).is_some() {
|
||||
@@ -1107,6 +1111,21 @@ impl Project {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let store = cx.global::<SettingsStore>();
|
||||
for worktree in self.worktrees(cx) {
|
||||
let worktree_id = worktree.read(cx).id().to_proto();
|
||||
for (path, content) in store.local_settings(worktree.id()) {
|
||||
self.client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path.to_string_lossy().into(),
|
||||
content: Some(content),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
||||
let client = self.client.clone();
|
||||
self.client_state = Some(ProjectClientState::Local {
|
||||
@@ -1219,6 +1238,14 @@ impl Project {
|
||||
message_id: u32,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
for worktree in &self.worktrees {
|
||||
store
|
||||
.clear_local_settings(worktree.handle_id(), cx)
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
self.join_project_response_message_id = message_id;
|
||||
self.set_worktrees_from_proto(message.worktrees, cx)?;
|
||||
self.set_collaborators_from_proto(message.collaborators, cx)?;
|
||||
@@ -1593,7 +1620,7 @@ impl Project {
|
||||
&self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
|
||||
) -> Task<Result<()>> {
|
||||
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
||||
return Task::ready(Err(anyhow!("buffer doesn't have a file")));
|
||||
};
|
||||
@@ -2215,13 +2242,34 @@ impl Project {
|
||||
}
|
||||
|
||||
fn maintain_buffer_languages(
|
||||
languages: &LanguageRegistry,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<()> {
|
||||
let mut subscription = languages.subscribe();
|
||||
let mut prev_reload_count = languages.reload_count();
|
||||
cx.spawn_weak(|project, mut cx| async move {
|
||||
while let Some(()) = subscription.next().await {
|
||||
if let Some(project) = project.upgrade(&cx) {
|
||||
// If the language registry has been reloaded, then remove and
|
||||
// re-assign the languages on all open buffers.
|
||||
let reload_count = languages.reload_count();
|
||||
if reload_count > prev_reload_count {
|
||||
prev_reload_count = reload_count;
|
||||
project.update(&mut cx, |this, cx| {
|
||||
let buffers = this
|
||||
.opened_buffers
|
||||
.values()
|
||||
.filter_map(|b| b.upgrade(cx))
|
||||
.collect::<Vec<_>>();
|
||||
for buffer in buffers {
|
||||
if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() {
|
||||
this.unregister_buffer_from_language_servers(&buffer, &f, cx);
|
||||
buffer.update(cx, |buffer, cx| buffer.set_language(None, cx));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
let mut plain_text_buffers = Vec::new();
|
||||
let mut buffers_with_unknown_injections = Vec::new();
|
||||
@@ -2321,25 +2369,34 @@ impl Project {
|
||||
});
|
||||
|
||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||
if let Some(worktree) = file.worktree.read(cx).as_local() {
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_abs_path = worktree.abs_path().clone();
|
||||
self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
|
||||
let worktree = file.worktree.clone();
|
||||
if let Some(tree) = worktree.read(cx).as_local() {
|
||||
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_language_servers(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
worktree_path: Arc<Path>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if !language_settings(Some(&language.name()), cx).enable_language_server {
|
||||
if !language_settings(
|
||||
Some(&language),
|
||||
worktree
|
||||
.update(cx, |tree, cx| tree.root_file(cx))
|
||||
.map(|f| f as _)
|
||||
.as_ref(),
|
||||
cx,
|
||||
)
|
||||
.enable_language_server
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
for adapter in language.lsp_adapters() {
|
||||
let key = (worktree_id, adapter.name.clone());
|
||||
if self.language_server_ids.contains_key(&key) {
|
||||
@@ -2400,18 +2457,23 @@ impl Project {
|
||||
LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move {
|
||||
let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
|
||||
let language_server = pending_server.task.await.log_err()?;
|
||||
let language_server = language_server
|
||||
.initialize(initialization_options)
|
||||
.await
|
||||
.log_err()?;
|
||||
let this = this.upgrade(&cx)?;
|
||||
|
||||
language_server
|
||||
.on_notification::<lsp::notification::LogMessage, _>({
|
||||
move |params, mut cx| {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(Event::LanguageServerLog(server_id, params.message))
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_notification::<lsp::notification::PublishDiagnostics, _>({
|
||||
let this = this.downgrade();
|
||||
let adapter = adapter.clone();
|
||||
move |mut params, cx| {
|
||||
let this = this;
|
||||
let adapter = adapter.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
adapter.process_diagnostics(&mut params).await;
|
||||
@@ -2463,8 +2525,7 @@ impl Project {
|
||||
// avoid stalling any language server like `gopls` which waits for a response
|
||||
// to these requests when initializing.
|
||||
language_server
|
||||
.on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
|
||||
let this = this.downgrade();
|
||||
.on_request::<lsp::request::WorkDoneProgressCreate, _, _>(
|
||||
move |params, mut cx| async move {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
@@ -2478,12 +2539,11 @@ impl Project {
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
language_server
|
||||
.on_request::<lsp::request::RegisterCapability, _, _>({
|
||||
let this = this.downgrade();
|
||||
.on_request::<lsp::request::RegisterCapability, _, _>(
|
||||
move |params, mut cx| async move {
|
||||
let this = this
|
||||
.upgrade(&cx)
|
||||
@@ -2501,24 +2561,15 @@ impl Project {
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
|
||||
let this = this.downgrade();
|
||||
let adapter = adapter.clone();
|
||||
let language_server = language_server.clone();
|
||||
move |params, cx| {
|
||||
Self::on_lsp_workspace_edit(
|
||||
this,
|
||||
params,
|
||||
server_id,
|
||||
adapter.clone(),
|
||||
language_server.clone(),
|
||||
cx,
|
||||
)
|
||||
Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -2528,7 +2579,6 @@ impl Project {
|
||||
|
||||
language_server
|
||||
.on_notification::<lsp::notification::Progress, _>({
|
||||
let this = this.downgrade();
|
||||
move |params, mut cx| {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -2544,6 +2594,10 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let language_server = language_server
|
||||
.initialize(initialization_options)
|
||||
.await
|
||||
.log_err()?;
|
||||
language_server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
lsp::DidChangeConfigurationParams {
|
||||
@@ -2552,6 +2606,7 @@ impl Project {
|
||||
)
|
||||
.ok();
|
||||
|
||||
let this = this.upgrade(&cx)?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// If the language server for this key doesn't match the server id, don't store the
|
||||
// server. Which will cause it to be dropped, killing the process
|
||||
@@ -2586,6 +2641,8 @@ impl Project {
|
||||
},
|
||||
);
|
||||
|
||||
cx.emit(Event::LanguageServerAdded(server_id));
|
||||
|
||||
if let Some(project_id) = this.remote_id() {
|
||||
this.client
|
||||
.send(proto::StartLanguageServer {
|
||||
@@ -2711,6 +2768,7 @@ impl Project {
|
||||
cx.notify();
|
||||
|
||||
let server_state = self.language_servers.remove(&server_id);
|
||||
cx.emit(Event::LanguageServerRemoved(server_id));
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let mut root_path = None;
|
||||
|
||||
@@ -2748,23 +2806,22 @@ impl Project {
|
||||
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
|
||||
let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = buffers
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let file = File::from_dyn(buffer.file())?;
|
||||
let worktree = file.worktree.read(cx).as_local()?;
|
||||
let full_path = file.full_path(cx);
|
||||
let language = self
|
||||
.languages
|
||||
.language_for_file(&full_path, Some(buffer.as_rope()))
|
||||
.now_or_never()?
|
||||
.ok()?;
|
||||
Some((worktree.id(), worktree.abs_path().clone(), language))
|
||||
Some((file.worktree.clone(), language))
|
||||
})
|
||||
.collect();
|
||||
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
|
||||
self.restart_language_servers(worktree_id, worktree_abs_path, language, cx);
|
||||
for (worktree, language) in language_server_lookup_info {
|
||||
self.restart_language_servers(worktree, language, cx);
|
||||
}
|
||||
|
||||
None
|
||||
@@ -2773,11 +2830,13 @@ impl Project {
|
||||
// TODO This will break in the case where the adapter's root paths and worktrees are not equal
|
||||
fn restart_language_servers(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
fallback_path: Arc<Path>,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let fallback_path = worktree.read(cx).abs_path();
|
||||
|
||||
let mut stops = Vec::new();
|
||||
for adapter in language.lsp_adapters() {
|
||||
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
|
||||
@@ -2807,7 +2866,7 @@ impl Project {
|
||||
.map(|path_buf| Arc::from(path_buf.as_path()))
|
||||
.unwrap_or(fallback_path);
|
||||
|
||||
this.start_language_servers(worktree_id, root_path, language.clone(), cx);
|
||||
this.start_language_servers(&worktree, root_path, language.clone(), cx);
|
||||
|
||||
// Lookup new server ids and set them for each of the orphaned worktrees
|
||||
for adapter in language.lsp_adapters() {
|
||||
@@ -3054,12 +3113,14 @@ impl Project {
|
||||
params: lsp::ApplyWorkspaceEditParams,
|
||||
server_id: LanguageServerId,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language_server: Arc<LanguageServer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<lsp::ApplyWorkspaceEditResponse> {
|
||||
let this = this
|
||||
.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("project project closed"))?;
|
||||
let language_server = this
|
||||
.read_with(&cx, |this, _| this.language_server_for_id(server_id))
|
||||
.ok_or_else(|| anyhow!("language server not found"))?;
|
||||
let transaction = Self::deserialize_workspace_edit(
|
||||
this.clone(),
|
||||
params.edit,
|
||||
@@ -3432,8 +3493,7 @@ impl Project {
|
||||
let mut project_transaction = ProjectTransaction::default();
|
||||
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
|
||||
let settings = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).clone()
|
||||
language_settings(buffer.language(), buffer.file(), cx).clone()
|
||||
});
|
||||
|
||||
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||
@@ -4020,7 +4080,7 @@ impl Project {
|
||||
let end_within = range.start.cmp(&primary.end, buffer).is_le()
|
||||
&& range.end.cmp(&primary.end, buffer).is_ge();
|
||||
|
||||
//Skip addtional edits which overlap with the primary completion edit
|
||||
//Skip additional edits which overlap with the primary completion edit
|
||||
//https://github.com/zed-industries/zed/pull/1871
|
||||
if !start_within && !end_within {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
@@ -4463,11 +4523,14 @@ impl Project {
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
let tab_size = buffer.read_with(cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
|
||||
let position = position.to_point_utf16(buffer);
|
||||
(
|
||||
position,
|
||||
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
|
||||
.tab_size,
|
||||
)
|
||||
});
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
OnTypeFormatting {
|
||||
@@ -4873,6 +4936,7 @@ impl Project {
|
||||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
|
||||
@@ -4893,8 +4957,12 @@ impl Project {
|
||||
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
||||
}
|
||||
|
||||
cx.observe_release(worktree, |this, worktree, cx| {
|
||||
let handle_id = worktree.id();
|
||||
cx.observe_release(worktree, move |this, worktree, cx| {
|
||||
let _ = this.remove_worktree(worktree.id(), cx);
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.clear_local_settings(handle_id, cx).log_err()
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -5095,9 +5163,9 @@ impl Project {
|
||||
return None;
|
||||
}
|
||||
let path = &project_path.path;
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| path.starts_with(work_dir))?;
|
||||
let receiver = receiver.clone();
|
||||
let path = path.clone();
|
||||
Some(async move {
|
||||
@@ -5120,9 +5188,9 @@ impl Project {
|
||||
return None;
|
||||
}
|
||||
let path = file.path();
|
||||
changed_repos.iter().find(|(work_dir, change)| {
|
||||
path.starts_with(work_dir) && change.git_dir_changed
|
||||
})?;
|
||||
changed_repos
|
||||
.iter()
|
||||
.find(|(work_dir, _)| path.starts_with(work_dir))?;
|
||||
Some((buffer, path.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -5179,6 +5247,71 @@ impl Project {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_local_worktree_settings(
|
||||
&mut self,
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
changes: &UpdatedEntriesSet,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let project_id = self.remote_id();
|
||||
let worktree_id = worktree.id();
|
||||
let worktree = worktree.read(cx).as_local().unwrap();
|
||||
let remote_worktree_id = worktree.id();
|
||||
|
||||
let mut settings_contents = Vec::new();
|
||||
for (path, _, change) in changes.iter() {
|
||||
if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
|
||||
let settings_dir = Arc::from(
|
||||
path.ancestors()
|
||||
.nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
|
||||
.unwrap(),
|
||||
);
|
||||
let fs = self.fs.clone();
|
||||
let removed = *change == PathChange::Removed;
|
||||
let abs_path = worktree.absolutize(path);
|
||||
settings_contents.push(async move {
|
||||
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if settings_contents.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.spawn_weak(move |_, mut cx| async move {
|
||||
let settings_contents: Vec<(Arc<Path>, _)> =
|
||||
futures::future::join_all(settings_contents).await;
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
for (directory, file_content) in settings_contents {
|
||||
let file_content = file_content.and_then(|content| content.log_err());
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree_id,
|
||||
directory.clone(),
|
||||
file_content.as_ref().map(String::as_str),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
if let Some(remote_id) = project_id {
|
||||
client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id: remote_id,
|
||||
worktree_id: remote_worktree_id.to_proto(),
|
||||
path: directory.to_string_lossy().into_owned(),
|
||||
content: file_content,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
@@ -5431,6 +5564,30 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_worktree_settings(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree.id(),
|
||||
PathBuf::from(&envelope.payload.path).into(),
|
||||
envelope.payload.content.as_ref().map(String::as_str),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_create_project_entry(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::CreateProjectEntry>,
|
||||
@@ -5834,16 +5991,15 @@ impl Project {
|
||||
.await?;
|
||||
let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id());
|
||||
|
||||
let (saved_version, fingerprint, mtime) = this
|
||||
.update(&mut cx, |this, cx| this.save_buffer(buffer, cx))
|
||||
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))
|
||||
.await?;
|
||||
Ok(proto::BufferSaved {
|
||||
Ok(buffer.read_with(&cx, |buffer, _| proto::BufferSaved {
|
||||
project_id,
|
||||
buffer_id,
|
||||
version: serialize_version(&saved_version),
|
||||
mtime: Some(mtime.into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(fingerprint),
|
||||
})
|
||||
version: serialize_version(buffer.saved_version()),
|
||||
mtime: Some(buffer.saved_mtime().into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn handle_reload_buffers(
|
||||
@@ -6521,8 +6677,8 @@ impl Project {
|
||||
}
|
||||
|
||||
self.metadata_changed(cx);
|
||||
for (id, _) in old_worktrees_by_id {
|
||||
cx.emit(Event::WorktreeRemoved(id));
|
||||
for id in old_worktrees_by_id.keys() {
|
||||
cx.emit(Event::WorktreeRemoved(*id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -6892,6 +7048,13 @@ impl WorktreeHandle {
|
||||
WorktreeHandle::Weak(handle) => handle.upgrade(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_id(&self) -> usize {
|
||||
match self {
|
||||
WorktreeHandle::Strong(handle) => handle.id(),
|
||||
WorktreeHandle::Weak(handle) => handle.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenBuffer {
|
||||
|
||||
@@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_project_specific_settings(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 8 }"#
|
||||
},
|
||||
"a": {
|
||||
"a.rs": "fn a() {\n A\n}"
|
||||
},
|
||||
"b": {
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 2 }"#
|
||||
},
|
||||
"b.rs": "fn b() {\n B\n}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
|
||||
deterministic.run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = worktree.read(cx);
|
||||
|
||||
let settings_a = language_settings(
|
||||
None,
|
||||
Some(
|
||||
&(File::for_entry(
|
||||
tree.entry_for_path("a/a.rs").unwrap().clone(),
|
||||
worktree.clone(),
|
||||
) as _),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let settings_b = language_settings(
|
||||
None,
|
||||
Some(
|
||||
&(File::for_entry(
|
||||
tree.entry_for_path("b/b.rs").unwrap().clone(),
|
||||
worktree.clone(),
|
||||
) as _),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(settings_a.tab_size.get(), 8);
|
||||
assert_eq!(settings_b.tab_size.get(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_language_servers(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -766,6 +826,11 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
let mut events = subscribe(&project, cx);
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
Event::LanguageServerAdded(LanguageServerId(0)),
|
||||
);
|
||||
|
||||
fake_server
|
||||
.start_progress(format!("{}/0", progress_token))
|
||||
.await;
|
||||
@@ -893,6 +958,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
|
||||
// Simulate the newly started server sending more diagnostics.
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
Event::LanguageServerAdded(LanguageServerId(1))
|
||||
);
|
||||
fake_server.start_progress(progress_token).await;
|
||||
assert_eq!(
|
||||
events.next().await.unwrap(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1523
crates/project/src/worktree_tests.rs
Normal file
1523
crates/project/src/worktree_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1002,6 +1002,7 @@ impl ProjectPanel {
|
||||
mtime: entry.mtime,
|
||||
is_symlink: false,
|
||||
is_ignored: false,
|
||||
git_status: entry.git_status,
|
||||
});
|
||||
}
|
||||
if expanded_dir_ids.binary_search(&entry.id).is_err()
|
||||
@@ -1011,6 +1012,9 @@ impl ProjectPanel {
|
||||
}
|
||||
entry_iter.advance();
|
||||
}
|
||||
|
||||
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
|
||||
|
||||
visible_worktree_entries.sort_by(|entry_a, entry_b| {
|
||||
let mut components_a = entry_a.path.components().peekable();
|
||||
let mut components_b = entry_b.path.components().peekable();
|
||||
@@ -1108,14 +1112,8 @@ impl ProjectPanel {
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for (entry, repo) in
|
||||
snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
|
||||
{
|
||||
let status = (git_status_setting
|
||||
&& entry.path.parent().is_some()
|
||||
&& !entry.is_ignored)
|
||||
.then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
|
||||
.flatten();
|
||||
for entry in visible_worktree_entries[entry_range].iter() {
|
||||
let status = git_status_setting.then(|| entry.git_status).flatten();
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set up fake langauge server to return fuzzy matches against
|
||||
// Set up fake language server to return fuzzy matches against
|
||||
// a fixed set of symbol names.
|
||||
let fake_symbols = [
|
||||
symbol("one", "/external"),
|
||||
|
||||
@@ -179,7 +179,11 @@ impl Rope {
|
||||
}
|
||||
|
||||
pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes {
|
||||
Bytes::new(self, range)
|
||||
Bytes::new(self, range, false)
|
||||
}
|
||||
|
||||
pub fn reversed_bytes_in_range(&self, range: Range<usize>) -> Bytes {
|
||||
Bytes::new(self, range, true)
|
||||
}
|
||||
|
||||
pub fn chunks(&self) -> Chunks {
|
||||
@@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
pub struct Bytes<'a> {
|
||||
chunks: sum_tree::Cursor<'a, Chunk, usize>,
|
||||
range: Range<usize>,
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl<'a> Bytes<'a> {
|
||||
pub fn new(rope: &'a Rope, range: Range<usize>) -> Self {
|
||||
pub fn new(rope: &'a Rope, range: Range<usize>, reversed: bool) -> Self {
|
||||
let mut chunks = rope.chunks.cursor();
|
||||
chunks.seek(&range.start, Bias::Right, &());
|
||||
Self { chunks, range }
|
||||
if reversed {
|
||||
chunks.seek(&range.end, Bias::Left, &());
|
||||
} else {
|
||||
chunks.seek(&range.start, Bias::Right, &());
|
||||
}
|
||||
Self {
|
||||
chunks,
|
||||
range,
|
||||
reversed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&'a [u8]> {
|
||||
let chunk = self.chunks.item()?;
|
||||
if self.reversed && self.range.start >= self.chunks.end(&()) {
|
||||
return None;
|
||||
}
|
||||
let chunk_start = *self.chunks.start();
|
||||
if self.range.end <= chunk_start {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = self.range.start.saturating_sub(chunk_start);
|
||||
let end = self.range.end - chunk_start;
|
||||
Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)])
|
||||
@@ -607,7 +622,11 @@ impl<'a> Iterator for Bytes<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.peek();
|
||||
if result.is_some() {
|
||||
self.chunks.next(&());
|
||||
if self.reversed {
|
||||
self.chunks.prev(&());
|
||||
} else {
|
||||
self.chunks.next(&());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -617,10 +636,21 @@ impl<'a> io::Read for Bytes<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if let Some(chunk) = self.peek() {
|
||||
let len = cmp::min(buf.len(), chunk.len());
|
||||
buf[..len].copy_from_slice(&chunk[..len]);
|
||||
self.range.start += len;
|
||||
if self.reversed {
|
||||
buf[..len].copy_from_slice(&chunk[chunk.len() - len..]);
|
||||
buf[..len].reverse();
|
||||
self.range.end -= len;
|
||||
} else {
|
||||
buf[..len].copy_from_slice(&chunk[..len]);
|
||||
self.range.start += len;
|
||||
}
|
||||
|
||||
if len == chunk.len() {
|
||||
self.chunks.next(&());
|
||||
if self.reversed {
|
||||
self.chunks.prev(&());
|
||||
} else {
|
||||
self.chunks.next(&());
|
||||
}
|
||||
}
|
||||
Ok(len)
|
||||
} else {
|
||||
|
||||
@@ -132,6 +132,8 @@ message Envelope {
|
||||
|
||||
OnTypeFormatting on_type_formatting = 111;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 113;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +341,13 @@ message UpdateWorktree {
|
||||
string abs_path = 10;
|
||||
}
|
||||
|
||||
message UpdateWorktreeSettings {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
string path = 3;
|
||||
optional string content = 4;
|
||||
}
|
||||
|
||||
message CreateProjectEntry {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
@@ -467,7 +476,7 @@ message Symbol {
|
||||
string name = 4;
|
||||
int32 kind = 5;
|
||||
string path = 6;
|
||||
// Cannot use generate anchors for unopend files,
|
||||
// Cannot use generate anchors for unopened files,
|
||||
// so we are forced to use point coords instead
|
||||
PointUtf16 start = 7;
|
||||
PointUtf16 end = 8;
|
||||
@@ -996,13 +1005,12 @@ message Entry {
|
||||
Timestamp mtime = 5;
|
||||
bool is_symlink = 6;
|
||||
bool is_ignored = 7;
|
||||
optional GitStatus git_status = 8;
|
||||
}
|
||||
|
||||
message RepositoryEntry {
|
||||
uint64 work_directory_id = 1;
|
||||
optional string branch = 2;
|
||||
repeated string removed_repo_paths = 3;
|
||||
repeated StatusEntry updated_statuses = 4;
|
||||
}
|
||||
|
||||
message StatusEntry {
|
||||
|
||||
@@ -42,7 +42,7 @@ impl PublicKey {
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Decrypt a base64-encoded string that was encrypted by the correspoding public key.
|
||||
/// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
|
||||
pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
|
||||
let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE)
|
||||
.context("failed to base64-decode encrypted string")?;
|
||||
|
||||
@@ -236,6 +236,7 @@ messages!(
|
||||
(UpdateProject, Foreground),
|
||||
(UpdateProjectCollaborator, Foreground),
|
||||
(UpdateWorktree, Foreground),
|
||||
(UpdateWorktreeSettings, Foreground),
|
||||
(UpdateDiffBase, Foreground),
|
||||
(GetPrivateUserInfo, Foreground),
|
||||
(GetPrivateUserInfoResponse, Foreground),
|
||||
@@ -345,6 +346,7 @@ entity_messages!(
|
||||
UpdateProject,
|
||||
UpdateProjectCollaborator,
|
||||
UpdateWorktree,
|
||||
UpdateWorktreeSettings,
|
||||
UpdateDiffBase
|
||||
);
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 56;
|
||||
pub const PROTOCOL_VERSION: u32 = 58;
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Not, Range},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -242,7 +242,13 @@ impl View for ProjectSearchView {
|
||||
|
||||
impl Item for ProjectSearchView {
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||
Some(self.query_editor.read(cx).text(cx).into())
|
||||
let query_text = self.query_editor.read(cx).text(cx);
|
||||
|
||||
query_text
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| query_text.into())
|
||||
.or_else(|| Some("Project Search".into()))
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
|
||||
@@ -12,7 +12,6 @@ doctest = false
|
||||
test-support = ["gpui/test-support", "fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
@@ -25,13 +24,14 @@ futures.workspace = true
|
||||
json_comments = "0.2"
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.workspace = true
|
||||
toml.workspace = true
|
||||
tree-sitter = "*"
|
||||
tree-sitter.workspace = true
|
||||
tree-sitter-json = "*"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::settings_store::parse_json_with_comments;
|
||||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||
use anyhow::{Context, Result};
|
||||
use assets::Assets;
|
||||
use collections::BTreeMap;
|
||||
use gpui::{keymap_matcher::Binding, AppContext};
|
||||
use schemars::{
|
||||
@@ -10,11 +9,11 @@ use schemars::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use util::ResultExt;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct KeymapFileContent(Vec<KeymapBlock>);
|
||||
pub struct KeymapFile(Vec<KeymapBlock>);
|
||||
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct KeymapBlock {
|
||||
@@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
|
||||
#[derive(Deserialize)]
|
||||
struct ActionWithData(Box<str>, Box<RawValue>);
|
||||
|
||||
impl KeymapFileContent {
|
||||
impl KeymapFile {
|
||||
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
||||
let content = Assets::get(asset_path).unwrap().data;
|
||||
let content_str = std::str::from_utf8(content.as_ref()).unwrap();
|
||||
Self::parse(content_str)?.add_to_cx(cx)
|
||||
let content = asset_str::<SettingsAssets>(asset_path);
|
||||
Self::parse(content.as_ref())?.add_to_cx(cx)
|
||||
}
|
||||
|
||||
pub fn parse(content: &str) -> Result<Self> {
|
||||
@@ -83,40 +81,40 @@ impl KeymapFileContent {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value {
|
||||
let mut root_schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<KeymapFileContent>();
|
||||
pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
|
||||
let mut root_schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<KeymapFile>();
|
||||
|
||||
let action_schema = Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
enum_values: Some(
|
||||
action_names
|
||||
.iter()
|
||||
.map(|name| Value::String(name.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||
..Default::default()
|
||||
}),
|
||||
]),
|
||||
let action_schema = Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
enum_values: Some(
|
||||
action_names
|
||||
.iter()
|
||||
.map(|name| Value::String(name.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||
..Default::default()
|
||||
}),
|
||||
]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,37 @@ mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
||||
use gpui::AssetSource;
|
||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str};
|
||||
use util::asset_str;
|
||||
|
||||
pub use keymap_file::KeymapFile;
|
||||
pub use settings_file::*;
|
||||
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
|
||||
use std::{borrow::Cow, str};
|
||||
|
||||
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
||||
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../assets"]
|
||||
#[include = "settings/*"]
|
||||
#[include = "keymaps/*"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct SettingsAssets;
|
||||
|
||||
pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
|
||||
match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
}
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/default.json")
|
||||
}
|
||||
|
||||
pub fn default_keymap() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("keymaps/default.json")
|
||||
}
|
||||
|
||||
pub fn vim_keymap() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("keymaps/vim.json")
|
||||
}
|
||||
|
||||
pub fn initial_user_settings_content() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/initial_user_settings.json")
|
||||
}
|
||||
|
||||
pub fn initial_local_settings_content() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/initial_local_settings.json")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH};
|
||||
use crate::{settings_store::SettingsStore, Setting};
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{executor::Background, AppContext, AssetSource};
|
||||
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
|
||||
use gpui::{executor::Background, AppContext};
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{paths, ResultExt};
|
||||
|
||||
pub fn register<T: Setting>(cx: &mut AppContext) {
|
||||
@@ -17,11 +22,8 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
|
||||
cx.global::<SettingsStore>().get(None)
|
||||
}
|
||||
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
}
|
||||
pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
|
||||
cx.global::<SettingsStore>().get(location)
|
||||
}
|
||||
|
||||
pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
|
||||
@@ -29,7 +31,7 @@ pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_settings() -> String {
|
||||
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
|
||||
default_settings().as_ref(),
|
||||
crate::default_settings().as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
util::merge_non_null_json_value_into(
|
||||
@@ -55,15 +57,22 @@ pub fn watch_config_file(
|
||||
.spawn(async move {
|
||||
let events = fs.watch(&path, Duration::from_millis(100)).await;
|
||||
futures::pin_mut!(events);
|
||||
|
||||
let contents = fs.load(&path).await.unwrap_or_default();
|
||||
if tx.unbounded_send(contents).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
if events.next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(contents) = fs.load(&path).await {
|
||||
if !tx.unbounded_send(contents).is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if events.next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -101,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
|
||||
Err(err) => {
|
||||
if let Some(e) = err.downcast_ref::<std::io::Error>() {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return Ok(crate::initial_user_settings_content(&Assets).to_string());
|
||||
return Ok(crate::initial_user_settings_content().to_string());
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
||||
use gpui::AppContext;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> {
|
||||
}
|
||||
|
||||
/// A set of strongly-typed setting values defined via multiple JSON files.
|
||||
#[derive(Default)]
|
||||
pub struct SettingsStore {
|
||||
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
||||
default_deserialized_settings: Option<serde_json::Value>,
|
||||
user_deserialized_settings: Option<serde_json::Value>,
|
||||
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
|
||||
raw_default_settings: serde_json::Value,
|
||||
raw_user_settings: serde_json::Value,
|
||||
raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
|
||||
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
|
||||
}
|
||||
|
||||
impl Default for SettingsStore {
|
||||
fn default() -> Self {
|
||||
SettingsStore {
|
||||
setting_values: Default::default(),
|
||||
raw_default_settings: serde_json::json!({}),
|
||||
raw_user_settings: serde_json::json!({}),
|
||||
raw_local_settings: Default::default(),
|
||||
tab_size_callback: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SettingValue<T> {
|
||||
global_value: Option<T>,
|
||||
local_values: Vec<(Arc<Path>, T)>,
|
||||
local_values: Vec<(usize, Arc<Path>, T)>,
|
||||
}
|
||||
|
||||
trait AnySettingValue {
|
||||
@@ -109,9 +120,9 @@ trait AnySettingValue {
|
||||
custom: &[DeserializedSetting],
|
||||
cx: &AppContext,
|
||||
) -> Result<Box<dyn Any>>;
|
||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
|
||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
|
||||
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
|
||||
fn json_schema(
|
||||
&self,
|
||||
generator: &mut SchemaGenerator,
|
||||
@@ -136,27 +147,24 @@ impl SettingsStore {
|
||||
local_values: Vec::new(),
|
||||
}));
|
||||
|
||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
||||
if let Some(default_settings) = setting_value
|
||||
.deserialize_setting(default_settings)
|
||||
if let Some(default_settings) = setting_value
|
||||
.deserialize_setting(&self.raw_default_settings)
|
||||
.log_err()
|
||||
{
|
||||
let mut user_values_stack = Vec::new();
|
||||
|
||||
if let Some(user_settings) = setting_value
|
||||
.deserialize_setting(&self.raw_user_settings)
|
||||
.log_err()
|
||||
{
|
||||
let mut user_values_stack = Vec::new();
|
||||
user_values_stack = vec![user_settings];
|
||||
}
|
||||
|
||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
||||
if let Some(user_settings) =
|
||||
setting_value.deserialize_setting(user_settings).log_err()
|
||||
{
|
||||
user_values_stack = vec![user_settings];
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(setting) = setting_value
|
||||
.load_setting(&default_settings, &user_values_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(setting);
|
||||
}
|
||||
if let Some(setting) = setting_value
|
||||
.load_setting(&default_settings, &user_values_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +173,7 @@ impl SettingsStore {
|
||||
///
|
||||
/// Panics if the given setting type has not been registered, or if there is no
|
||||
/// value for this setting.
|
||||
pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
|
||||
pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
|
||||
self.setting_values
|
||||
.get(&TypeId::of::<T>())
|
||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||
@@ -188,10 +196,8 @@ impl SettingsStore {
|
||||
///
|
||||
/// This is only for debugging and reporting. For user-facing functionality,
|
||||
/// use the typed setting interface.
|
||||
pub fn untyped_user_settings(&self) -> &serde_json::Value {
|
||||
self.user_deserialized_settings
|
||||
.as_ref()
|
||||
.unwrap_or(&serde_json::Value::Null)
|
||||
pub fn raw_user_settings(&self) -> &serde_json::Value {
|
||||
&self.raw_user_settings
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -213,11 +219,7 @@ impl SettingsStore {
|
||||
cx: &AppContext,
|
||||
update: impl FnOnce(&mut T::FileContent),
|
||||
) {
|
||||
if self.user_deserialized_settings.is_none() {
|
||||
self.set_user_settings("{}", cx).unwrap();
|
||||
}
|
||||
let old_text =
|
||||
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
|
||||
let old_text = serde_json::to_string(&self.raw_user_settings).unwrap();
|
||||
let new_text = self.new_text_for_update::<T>(old_text, update);
|
||||
self.set_user_settings(&new_text, cx).unwrap();
|
||||
}
|
||||
@@ -246,29 +248,19 @@ impl SettingsStore {
|
||||
) -> Vec<(Range<usize>, String)> {
|
||||
let setting_type_id = TypeId::of::<T>();
|
||||
|
||||
let old_content = self
|
||||
let setting = self
|
||||
.setting_values
|
||||
.get(&setting_type_id)
|
||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||
.deserialize_setting(
|
||||
self.user_deserialized_settings
|
||||
.as_ref()
|
||||
.expect("no user settings loaded"),
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"could not deserialize setting type {} from user settings: {}",
|
||||
type_name::<T>(),
|
||||
e
|
||||
)
|
||||
})
|
||||
.0
|
||||
.downcast::<T::FileContent>()
|
||||
.unwrap();
|
||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()));
|
||||
let raw_settings = parse_json_with_comments::<serde_json::Value>(text).unwrap_or_default();
|
||||
let old_content = match setting.deserialize_setting(&raw_settings) {
|
||||
Ok(content) => content.0.downcast::<T::FileContent>().unwrap(),
|
||||
Err(_) => Box::new(T::FileContent::default()),
|
||||
};
|
||||
let mut new_content = old_content.clone();
|
||||
update(&mut new_content);
|
||||
|
||||
let old_value = &serde_json::to_value(&old_content).unwrap();
|
||||
let old_value = serde_json::to_value(&old_content).unwrap();
|
||||
let new_value = serde_json::to_value(new_content).unwrap();
|
||||
|
||||
let mut key_path = Vec::new();
|
||||
@@ -323,10 +315,14 @@ impl SettingsStore {
|
||||
default_settings_content: &str,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
self.default_deserialized_settings =
|
||||
Some(parse_json_with_comments(default_settings_content)?);
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
|
||||
if settings.is_object() {
|
||||
self.raw_default_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("settings must be an object"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user settings via a JSON string.
|
||||
@@ -335,28 +331,47 @@ impl SettingsStore {
|
||||
user_settings_content: &str,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
|
||||
if settings.is_object() {
|
||||
self.raw_user_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("settings must be an object"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or remove a set of local settings via a JSON string.
|
||||
pub fn set_local_settings(
|
||||
&mut self,
|
||||
root_id: usize,
|
||||
path: Arc<Path>,
|
||||
settings_content: Option<&str>,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some(content) = settings_content {
|
||||
self.local_deserialized_settings
|
||||
.insert(path.clone(), parse_json_with_comments(content)?);
|
||||
self.raw_local_settings
|
||||
.insert((root_id, path.clone()), parse_json_with_comments(content)?);
|
||||
} else {
|
||||
self.local_deserialized_settings.remove(&path);
|
||||
self.raw_local_settings.remove(&(root_id, path.clone()));
|
||||
}
|
||||
self.recompute_values(Some(&path), cx)?;
|
||||
self.recompute_values(Some((root_id, &path)), cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add or remove a set of local settings via a JSON string.
|
||||
pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
|
||||
self.raw_local_settings.retain(|k, _| k.0 != root_id);
|
||||
self.recompute_values(Some((root_id, "".as_ref())), cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
|
||||
self.raw_local_settings
|
||||
.range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
|
||||
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
|
||||
}
|
||||
|
||||
pub fn json_schema(
|
||||
&self,
|
||||
schema_params: &SettingsJsonSchemaParams,
|
||||
@@ -436,72 +451,69 @@ impl SettingsStore {
|
||||
|
||||
fn recompute_values(
|
||||
&mut self,
|
||||
changed_local_path: Option<&Path>,
|
||||
changed_local_path: Option<(usize, &Path)>,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
// Reload the global and local values for every setting.
|
||||
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
|
||||
let mut paths_stack = Vec::<Option<&Path>>::new();
|
||||
let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
|
||||
for setting_value in self.setting_values.values_mut() {
|
||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
||||
let default_settings = setting_value.deserialize_setting(default_settings)?;
|
||||
let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?;
|
||||
|
||||
user_settings_stack.clear();
|
||||
paths_stack.clear();
|
||||
user_settings_stack.clear();
|
||||
paths_stack.clear();
|
||||
|
||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
||||
if let Some(user_settings) =
|
||||
setting_value.deserialize_setting(user_settings).log_err()
|
||||
{
|
||||
user_settings_stack.push(user_settings);
|
||||
paths_stack.push(None);
|
||||
if let Some(user_settings) = setting_value
|
||||
.deserialize_setting(&self.raw_user_settings)
|
||||
.log_err()
|
||||
{
|
||||
user_settings_stack.push(user_settings);
|
||||
paths_stack.push(None);
|
||||
}
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
for ((root_id, path), local_settings) in &self.raw_local_settings {
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_entry) = paths_stack.last() {
|
||||
if let Some((prev_root_id, prev_path)) = prev_entry {
|
||||
if root_id != prev_root_id || !path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
user_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
if let Some(local_settings) =
|
||||
setting_value.deserialize_setting(&local_settings).log_err()
|
||||
{
|
||||
paths_stack.push(Some((*root_id, path.as_ref())));
|
||||
user_settings_stack.push(local_settings);
|
||||
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
|
||||
*root_id != changed_root_id || !path.starts_with(changed_local_path)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
for (path, local_settings) in &self.local_deserialized_settings {
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_path) = paths_stack.last() {
|
||||
if let Some(prev_path) = prev_path {
|
||||
if !path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
user_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(local_settings) =
|
||||
setting_value.deserialize_setting(&local_settings).log_err()
|
||||
{
|
||||
paths_stack.push(Some(path.as_ref()));
|
||||
user_settings_stack.push(local_settings);
|
||||
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(false, |changed_local_path| {
|
||||
!path.starts_with(changed_local_path)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(path.clone(), value);
|
||||
}
|
||||
setting_value.set_local_value(*root_id, path.clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,6 +522,24 @@ impl SettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SettingsStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SettingsStore")
|
||||
.field(
|
||||
"types",
|
||||
&self
|
||||
.setting_values
|
||||
.values()
|
||||
.map(|value| value.setting_type_name())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.field("default_settings", &self.raw_default_settings)
|
||||
.field("user_settings", &self.raw_user_settings)
|
||||
.field("local_settings", &self.raw_local_settings)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
fn key(&self) -> Option<&'static str> {
|
||||
T::KEY
|
||||
@@ -546,10 +576,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
Ok(DeserializedSetting(Box::new(value)))
|
||||
}
|
||||
|
||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
|
||||
if let Some(path) = path {
|
||||
for (settings_path, value) in self.local_values.iter().rev() {
|
||||
if path.starts_with(&settings_path) {
|
||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
|
||||
if let Some((root_id, path)) = path {
|
||||
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
|
||||
if root_id == *settings_root_id && path.starts_with(&settings_path) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -563,11 +593,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
self.global_value = Some(*value.downcast().unwrap());
|
||||
}
|
||||
|
||||
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
|
||||
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
|
||||
let value = *value.downcast().unwrap();
|
||||
match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
|
||||
Ok(ix) => self.local_values[ix].1 = value,
|
||||
Err(ix) => self.local_values.insert(ix, (path, value)),
|
||||
match self
|
||||
.local_values
|
||||
.binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
|
||||
{
|
||||
Ok(ix) => self.local_values[ix].2 = value,
|
||||
Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,22 +614,6 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// impl Debug for SettingsStore {
|
||||
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// return f
|
||||
// .debug_struct("SettingsStore")
|
||||
// .field(
|
||||
// "setting_value_sets_by_type",
|
||||
// &self
|
||||
// .setting_values
|
||||
// .values()
|
||||
// .map(|set| (set.setting_type_name(), set))
|
||||
// .collect::<HashMap<_, _>>(),
|
||||
// )
|
||||
// .finish_non_exhaustive();
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update_value_in_json_text<'a>(
|
||||
text: &mut String,
|
||||
key_path: &mut Vec<&'a str>,
|
||||
@@ -639,6 +656,10 @@ fn update_value_in_json_text<'a>(
|
||||
key_path.pop();
|
||||
}
|
||||
} else if old_value != new_value {
|
||||
let mut new_value = new_value.clone();
|
||||
if let Some(new_object) = new_value.as_object_mut() {
|
||||
new_object.retain(|_, v| !v.is_null());
|
||||
}
|
||||
let (range, replacement) =
|
||||
replace_value_in_json_text(text, &key_path, tab_size, &new_value);
|
||||
text.replace_range(range.clone(), &replacement);
|
||||
@@ -650,7 +671,7 @@ fn replace_value_in_json_text(
|
||||
text: &str,
|
||||
key_path: &[&str],
|
||||
tab_size: usize,
|
||||
new_value: impl Serialize,
|
||||
new_value: &serde_json::Value,
|
||||
) -> (Range<usize>, String) {
|
||||
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
|
||||
const LANGUAGES: &'static str = "languages";
|
||||
@@ -884,6 +905,7 @@ mod tests {
|
||||
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root1").into(),
|
||||
Some(r#"{ "user": { "staff": true } }"#),
|
||||
cx,
|
||||
@@ -891,6 +913,7 @@ mod tests {
|
||||
.unwrap();
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root1/subdir").into(),
|
||||
Some(r#"{ "user": { "name": "Jane Doe" } }"#),
|
||||
cx,
|
||||
@@ -899,6 +922,7 @@ mod tests {
|
||||
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root2").into(),
|
||||
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
|
||||
cx,
|
||||
@@ -906,7 +930,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root1/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
|
||||
&UserSettings {
|
||||
name: "John Doe".to_string(),
|
||||
age: 31,
|
||||
@@ -914,7 +938,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
|
||||
&UserSettings {
|
||||
name: "Jane Doe".to_string(),
|
||||
age: 31,
|
||||
@@ -922,7 +946,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root2/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
|
||||
&UserSettings {
|
||||
name: "John Doe".to_string(),
|
||||
age: 42,
|
||||
@@ -930,7 +954,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
|
||||
store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
|
||||
&MultiKeySettings {
|
||||
key1: "a".to_string(),
|
||||
key2: "b".to_string(),
|
||||
@@ -994,24 +1018,32 @@ mod tests {
|
||||
r#"{
|
||||
"languages": {
|
||||
"JSON": {
|
||||
"is_enabled": true
|
||||
"language_setting_1": true
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
|settings| {
|
||||
settings.languages.get_mut("JSON").unwrap().is_enabled = false;
|
||||
settings
|
||||
.languages
|
||||
.insert("Rust".into(), LanguageSettingEntry { is_enabled: true });
|
||||
.get_mut("JSON")
|
||||
.unwrap()
|
||||
.language_setting_1 = Some(false);
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingEntry {
|
||||
language_setting_2: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
},
|
||||
r#"{
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"is_enabled": true
|
||||
"language_setting_2": true
|
||||
},
|
||||
"JSON": {
|
||||
"is_enabled": false
|
||||
"language_setting_1": false
|
||||
}
|
||||
}
|
||||
}"#
|
||||
@@ -1074,6 +1106,23 @@ mod tests {
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
check_settings_update::<UserSettings>(
|
||||
&mut store,
|
||||
r#"{
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.age = Some(37),
|
||||
r#"{
|
||||
"user": {
|
||||
"age": 37
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn check_settings_update<T: Setting>(
|
||||
@@ -1202,9 +1251,10 @@ mod tests {
|
||||
languages: HashMap<String, LanguageSettingEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct LanguageSettingEntry {
|
||||
is_enabled: bool,
|
||||
language_setting_1: Option<bool>,
|
||||
language_setting_2: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for LanguageSettings {
|
||||
|
||||
@@ -160,7 +160,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
||||
|
||||
// Create a one shot channel for the result of the queued write
|
||||
// so we can await on the result
|
||||
let (sender, reciever) = oneshot::channel();
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let thread_safe_connection = (*self).clone();
|
||||
write_channel(Box::new(move || {
|
||||
@@ -168,7 +168,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
||||
let result = connection.with_write(|connection| callback(connection));
|
||||
sender.send(result).ok();
|
||||
}));
|
||||
reciever.map(|response| response.expect("Write queue unexpectedly closed"))
|
||||
receiver.map(|response| response.expect("Write queue unexpectedly closed"))
|
||||
}
|
||||
|
||||
pub(crate) fn create_connection(
|
||||
@@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor {
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
Box::new(|| {
|
||||
let (sender, reciever) = channel::<QueuedWrite>();
|
||||
let (sender, receiver) = channel::<QueuedWrite>();
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(write) = reciever.recv() {
|
||||
while let Ok(write) = receiver.recv() {
|
||||
write()
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user