Compare commits
112 Commits
buffer-fon
...
markdown-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0649250a3 | ||
|
|
d7becce9aa | ||
|
|
62171387f6 | ||
|
|
47ad010901 | ||
|
|
06987edadb | ||
|
|
1e1a2807db | ||
|
|
9782dd342f | ||
|
|
535bcfad10 | ||
|
|
c76bacb974 | ||
|
|
20554d0296 | ||
|
|
2c78cf349b | ||
|
|
c81eb419d4 | ||
|
|
c4e446f8a8 | ||
|
|
bc7eaa6cd5 | ||
|
|
e93d554725 | ||
|
|
775539b3fa | ||
|
|
545319bced | ||
|
|
0b2de51c37 | ||
|
|
9a680dafc3 | ||
|
|
4c35cfaa69 | ||
|
|
be2bf98529 | ||
|
|
4eb1e65fbb | ||
|
|
52591905fb | ||
|
|
d2e83cc148 | ||
|
|
f633460a8d | ||
|
|
9470a52b5d | ||
|
|
fa0302f156 | ||
|
|
5d7148bde1 | ||
|
|
58991f332b | ||
|
|
9c569c8d95 | ||
|
|
1ba0bf925b | ||
|
|
53105ddd16 | ||
|
|
210f8ebfed | ||
|
|
c015b5c4cd | ||
|
|
c1c8a74c7f | ||
|
|
2f00fcbdf6 | ||
|
|
5c5fb972d0 | ||
|
|
7928095951 | ||
|
|
70c3ca4fdd | ||
|
|
d49271a112 | ||
|
|
e34c443331 | ||
|
|
263023021d | ||
|
|
7e1a184446 | ||
|
|
c834ea75ef | ||
|
|
4d8cba2add | ||
|
|
08aef198d5 | ||
|
|
2cfb1ffa77 | ||
|
|
f3192b6fa6 | ||
|
|
33b9aca090 | ||
|
|
57b087e41e | ||
|
|
2a9ce3cec3 | ||
|
|
f5c2483423 | ||
|
|
4d314b2dd0 | ||
|
|
7a112b22ac | ||
|
|
575eb792fb | ||
|
|
4f776f9ebe | ||
|
|
0c77e1ce45 | ||
|
|
904b740e16 | ||
|
|
f2fc84ab44 | ||
|
|
fda3c91f16 | ||
|
|
3eb8464d19 | ||
|
|
58f57491b1 | ||
|
|
3e44e97177 | ||
|
|
fda21232ae | ||
|
|
57a736d74a | ||
|
|
015e2ecd19 | ||
|
|
5037f466f6 | ||
|
|
f28fde5e58 | ||
|
|
d1928f084e | ||
|
|
ad22bddffa | ||
|
|
da0d968a2c | ||
|
|
200e36311c | ||
|
|
db48c75231 | ||
|
|
1911a9f39b | ||
|
|
faebce8cd0 | ||
|
|
573ba83034 | ||
|
|
97c5cffbe3 | ||
|
|
556ecd94c2 | ||
|
|
3289188e0a | ||
|
|
5e4f707951 | ||
|
|
5d7642d77d | ||
|
|
e64ecdc9ab | ||
|
|
ba9c5929af | ||
|
|
ad8dd1771a | ||
|
|
cb6d0639db | ||
|
|
065f15e9a6 | ||
|
|
104558115f | ||
|
|
4e6f24a841 | ||
|
|
f3a78f613a | ||
|
|
8bca9cea26 | ||
|
|
28586060a1 | ||
|
|
49371b44cb | ||
|
|
4b40e83b8b | ||
|
|
dffddaec4c | ||
|
|
a4d6c5da7c | ||
|
|
3ea17248c8 | ||
|
|
e0e1103228 | ||
|
|
65c9e7d3d1 | ||
|
|
b5b872656b | ||
|
|
f4d9a97195 | ||
|
|
7b01a29f5a | ||
|
|
04e89c4c51 | ||
|
|
0ab5a524b0 | ||
|
|
cd5ddfe34b | ||
|
|
0a4c3488dd | ||
|
|
a1cbc23fee | ||
|
|
298e9c9387 | ||
|
|
6e1ba7e936 | ||
|
|
bc0c2e0cae | ||
|
|
29a50573a9 | ||
|
|
08786fa7bf | ||
|
|
f2d61f3ea5 |
49
.github/workflows/bump_patch_version.yml
vendored
Normal file
49
.github/workflows/bump_patch_version.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: bump_patch_version
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Branch name to run on"
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.event.input.branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bump_patch_version:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
|
||||
|
||||
- name: Bump Patch Version
|
||||
run: |
|
||||
set -eux
|
||||
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
|
||||
tag_suffix=""
|
||||
case $channel in
|
||||
stable)
|
||||
;;
|
||||
preview)
|
||||
tag_suffix="-pre"
|
||||
;;
|
||||
*)
|
||||
echo "this must be run on either of stable|preview release branches" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl
|
||||
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
|
||||
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
|
||||
git tag v${output}${tag_suffix}
|
||||
git push origin HEAD v${output}${tag_suffix}
|
||||
@@ -11,7 +11,7 @@ If you're looking for ideas about what to work on, check out:
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
||||
|
||||
Outside of a handful of extremely popular languages and themes, we are generally not looking to extend Zed's language or theme support by directly building them into Zed. We really want to build a plugin system to handle making the editor extensible going forward. If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.
|
||||
For adding themes or support for a new language to Zed, check out our [extension docs](https://github.com/zed-industries/extensions/blob/main/AUTHORING_EXTENSIONS.md).
|
||||
|
||||
## Proposing changes
|
||||
|
||||
|
||||
346
Cargo.lock
generated
346
Cargo.lock
generated
@@ -520,7 +520,7 @@ dependencies = [
|
||||
"polling 3.3.2",
|
||||
"rustix 0.38.32",
|
||||
"slab",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -861,7 +861,7 @@ dependencies = [
|
||||
"ring 0.17.7",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -897,7 +897,7 @@ dependencies = [
|
||||
"http-body",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -926,7 +926,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"regex-lite",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -949,7 +949,7 @@ dependencies = [
|
||||
"http 0.2.9",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -971,7 +971,7 @@ dependencies = [
|
||||
"http 0.2.9",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -994,7 +994,7 @@ dependencies = [
|
||||
"http 0.2.9",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1022,7 +1022,7 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"subtle",
|
||||
"time",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1055,7 +1055,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"sha1",
|
||||
"sha2 0.10.7",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1087,7 +1087,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1131,7 +1131,7 @@ dependencies = [
|
||||
"pin-utils",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1146,7 +1146,7 @@ dependencies = [
|
||||
"http 0.2.9",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1194,7 +1194,7 @@ dependencies = [
|
||||
"aws-smithy-types",
|
||||
"http 0.2.9",
|
||||
"rustc_version",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1517,7 +1517,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-lite 2.2.0",
|
||||
"piper",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1578,17 +1578,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"lazy_static",
|
||||
"rayon",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.6.2"
|
||||
@@ -2286,7 +2275,7 @@ dependencies = [
|
||||
"toml 0.8.10",
|
||||
"tower",
|
||||
"tower-http 0.4.4",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -3276,6 +3265,15 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
|
||||
[[package]]
|
||||
name = "doxygen-rs"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9"
|
||||
dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dwrote"
|
||||
version = "0.11.0"
|
||||
@@ -4096,6 +4094,17 @@ dependencies = [
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-batch"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
|
||||
dependencies = [
|
||||
"futures 0.3.28",
|
||||
"futures-timer",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
@@ -4191,6 +4200,12 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
@@ -4316,11 +4331,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.15.0"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
|
||||
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.4.2",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
@@ -4558,7 +4573,7 @@ dependencies = [
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4670,6 +4685,41 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"heed-types",
|
||||
"libc",
|
||||
"lmdb-master-sys",
|
||||
"once_cell",
|
||||
"page_size",
|
||||
"serde",
|
||||
"synchronoise",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heed-traits"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
|
||||
[[package]]
|
||||
name = "heed-types"
|
||||
version = "0.20.0-alpha.9"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
@@ -4834,7 +4884,7 @@ dependencies = [
|
||||
"socket2 0.4.9",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
@@ -5149,7 +5199,7 @@ dependencies = [
|
||||
"polling 2.8.0",
|
||||
"slab",
|
||||
"sluice",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"url",
|
||||
"waker-fn",
|
||||
@@ -5437,7 +5487,6 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"regex",
|
||||
"rope",
|
||||
@@ -5463,19 +5512,16 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-gomod",
|
||||
"tree-sitter-gowork",
|
||||
"tree-sitter-hcl",
|
||||
"tree-sitter-heex",
|
||||
"tree-sitter-jsdoc",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-nu",
|
||||
"tree-sitter-proto",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-regex",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -5517,9 +5563,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.2+1.5.1"
|
||||
version = "0.16.2+1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
|
||||
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5676,6 +5722,16 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lmdb-master-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"doxygen-rs",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.10"
|
||||
@@ -6695,6 +6751,16 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.5"
|
||||
@@ -6868,9 +6934,33 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
@@ -7075,7 +7165,7 @@ dependencies = [
|
||||
"concurrent-queue",
|
||||
"pin-project-lite",
|
||||
"rustix 0.38.32",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -7943,7 +8033,6 @@ name = "rope"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bromberg_sl2",
|
||||
"criterion",
|
||||
"gpui",
|
||||
"log",
|
||||
@@ -7980,7 +8069,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"util",
|
||||
"zstd",
|
||||
]
|
||||
@@ -8347,7 +8436,7 @@ dependencies = [
|
||||
"strum",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
@@ -8486,6 +8575,35 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
|
||||
|
||||
[[package]]
|
||||
name = "semantic_index"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"futures-batch",
|
||||
"gpui",
|
||||
"heed",
|
||||
"language",
|
||||
"languages",
|
||||
"log",
|
||||
"open_ai",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
@@ -8500,12 +8618,6 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
|
||||
[[package]]
|
||||
name = "seq-macro"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.196"
|
||||
@@ -9098,7 +9210,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots",
|
||||
@@ -9185,7 +9297,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
@@ -9230,7 +9342,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
]
|
||||
@@ -9255,7 +9367,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sqlx-core",
|
||||
"time",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
@@ -9497,6 +9609,15 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "synchronoise"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.1"
|
||||
@@ -9720,6 +9841,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shellexpand",
|
||||
"shlex",
|
||||
"smol",
|
||||
"task",
|
||||
"terminal",
|
||||
@@ -10078,7 +10200,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10173,7 +10295,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10210,7 +10332,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10234,16 +10356,7 @@ dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10262,14 +10375,6 @@ name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -10282,32 +10387,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||
dependencies = [
|
||||
"pin-project",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.1.3"
|
||||
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
@@ -10318,8 +10426,8 @@ dependencies = [
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing 0.1.40 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
@@ -10417,15 +10525,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-hcl"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/MichaHoffmann/tree-sitter-hcl?rev=v1.1.0#636dbe70301ecbab8f353c8c78b3406fe4f185f5"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-heex"
|
||||
version = "0.0.1"
|
||||
@@ -10482,15 +10581,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-nu"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/nushell/tree-sitter-nu?rev=7dd29f9616822e5fc259f5b4ae6c4ded9a71a132#7dd29f9616822e5fc259f5b4ae6c4ded9a71a132"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-proto"
|
||||
version = "0.0.2"
|
||||
@@ -10549,15 +10639,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
@@ -11200,7 +11281,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"wasmtime",
|
||||
"wasmtime-c-api-macros",
|
||||
]
|
||||
@@ -11412,7 +11493,7 @@ dependencies = [
|
||||
"system-interface",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasmtime",
|
||||
"wiggle",
|
||||
@@ -11649,7 +11730,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bitflags 2.4.2",
|
||||
"thiserror",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"wasmtime",
|
||||
"wiggle-macro",
|
||||
]
|
||||
@@ -12405,7 +12486,7 @@ dependencies = [
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.52.0",
|
||||
"xdg-home",
|
||||
@@ -12542,14 +12623,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_clojure"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_csharp"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -12591,15 +12672,6 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.6"
|
||||
@@ -12622,7 +12694,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_gleam"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12643,7 +12715,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_lua"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -12657,7 +12729,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -12684,10 +12756,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
name = "zed_terraform"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.5",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12698,10 +12777,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
name = "zed_vue"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.5",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -73,6 +73,7 @@ members = [
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/settings",
|
||||
"crates/snippet",
|
||||
@@ -117,8 +118,10 @@ members = [
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/svelte",
|
||||
"extensions/terraform",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
"extensions/vue",
|
||||
"extensions/zig",
|
||||
|
||||
"tooling/xtask",
|
||||
@@ -252,9 +255,11 @@ derive_more = "0.99.17"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.9"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
git2 = { version = "0.18", default-features = false }
|
||||
globset = "0.4"
|
||||
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
ignore = "0.4.22"
|
||||
indoc = "1"
|
||||
@@ -293,6 +298,7 @@ serde_json_lenient = { version = "0.1", features = [
|
||||
] }
|
||||
serde_repr = "0.1"
|
||||
sha2 = "0.10"
|
||||
shlex = "1.3"
|
||||
shellexpand = "2.1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
@@ -322,28 +328,24 @@ tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
|
||||
rustc-demangle = "0.1.23"
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-regex = "0.20.0"
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
|
||||
unindent = "0.1.7"
|
||||
unicase = "2.6"
|
||||
unicode-segmentation = "1.10"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
||||
wasmparser = "0.201"
|
||||
wasm-encoder = "0.201"
|
||||
wasmtime = { version = "19.0.0", default-features = false, features = [
|
||||
|
||||
2
Procfile
2
Procfile
@@ -1,3 +1,3 @@
|
||||
collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve
|
||||
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve
|
||||
livekit: livekit-server --dev
|
||||
blob_store: ./script/run-local-minio
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Zed
|
||||
|
||||
[](https://github.com/zed-industries/ze34actions/workflows/ci.yml)
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
||||
@@ -38,6 +38,8 @@ brew install zed-preview
|
||||
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
|
||||
|
||||
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
|
||||
|
||||
## Licensing
|
||||
|
||||
License information for third party dependencies must be correctly provided for CI to pass.
|
||||
|
||||
3
assets/icons/sliders.svg
Normal file
3
assets/icons/sliders.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -234,6 +234,8 @@
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"shift-h": "vim::WindowTop",
|
||||
"shift-m": "vim::WindowMiddle",
|
||||
"shift-l": "vim::WindowBottom",
|
||||
@@ -367,6 +369,15 @@
|
||||
"< <": "vim::Outdent",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
@@ -532,6 +543,18 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal",
|
||||
"bindings": {
|
||||
"g c c": "editor::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
@@ -590,17 +613,18 @@
|
||||
"%": "project_panel::NewFile",
|
||||
"/": "project_panel::NewSearchInDirectory",
|
||||
"d": "project_panel::NewDirectory",
|
||||
"enter": "project_panel::Open",
|
||||
"enter": "project_panel::OpenPermanent",
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"o": "project_panel::Open",
|
||||
"o": "project_panel::OpenPermanent",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
"t": "project_panel::Open",
|
||||
"v": "project_panel::Open",
|
||||
"t": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,20 @@
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Centered layout related settings.
|
||||
"centered_layout": {
|
||||
// The relative width of the left padding of the central pane from the
|
||||
// workspace when the centered layout is used.
|
||||
"left_padding": 0.2,
|
||||
// The relative width of the right padding of the central pane from the
|
||||
// workspace when the centered layout is used.
|
||||
"right_padding": 0.2
|
||||
},
|
||||
// The key to use for adding multiple cursors
|
||||
// Currently "alt" or "cmd_or_ctrl" (also aliased as
|
||||
// "cmd" and "ctrl") are supported.
|
||||
"multi_cursor_modifier": "alt",
|
||||
// Whether to enable vim modes and key bindings
|
||||
// Whether to enable vim modes and key bindings.
|
||||
"vim_mode": false,
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
@@ -92,8 +101,9 @@
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
// Whether to automatically type closing characters for you. For example,
|
||||
// when you type (, Zed will automatically add a closing ) at the correct position.
|
||||
// Whether to automatically add matching closing characters when typing
|
||||
// opening parenthesis, bracket, brace, single or double quote characters.
|
||||
// For example, when you type (, Zed will add a closing ) at the correct position.
|
||||
"use_autoclose": true,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
// When set to `false`(default), skipping over and auto-removing of the closing characters
|
||||
@@ -145,10 +155,10 @@
|
||||
"show": "auto",
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true,
|
||||
// Whether to show selections in the scrollbar.
|
||||
"selections": true,
|
||||
// Whether to show symbols selections in the scrollbar.
|
||||
"symbols_selections": true,
|
||||
// Whether to show buffer search results in the scrollbar.
|
||||
"search_results": true,
|
||||
// Whether to show selected symbol occurrences in the scrollbar.
|
||||
"selected_symbol": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
@@ -171,6 +181,9 @@
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
"relative_line_numbers": false,
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
@@ -214,7 +227,10 @@
|
||||
// Whether to reveal it in the project panel automatically,
|
||||
// when a corresponding project entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true
|
||||
"auto_reveal_entries": true,
|
||||
/// Whether to fold directories automatically
|
||||
/// when a directory has only one directory inside.
|
||||
"auto_fold_dirs": false
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -387,7 +403,15 @@
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
"git_gutter": "tracked_files",
|
||||
// Control whether the git blame information is shown inline,
|
||||
// in the currently focused line.
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
}
|
||||
},
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
@@ -476,6 +500,8 @@
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Whether to show the terminal button in the status bar
|
||||
"button": true,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// environment. Use `:` to separate multiple values.
|
||||
"env": {
|
||||
|
||||
@@ -1119,8 +1119,8 @@ impl AssistantPanel {
|
||||
)
|
||||
.size_full()
|
||||
.into_any_element()
|
||||
} else {
|
||||
let editor = self.active_conversation_editor().unwrap();
|
||||
} else if let Some(editor) = self.active_conversation_editor() {
|
||||
let editor = editor.clone();
|
||||
let conversation = editor.read(cx).conversation.clone();
|
||||
div()
|
||||
.size_full()
|
||||
@@ -1135,6 +1135,8 @@ impl AssistantPanel {
|
||||
.children(self.render_remaining_tokens(&conversation, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -2065,7 +2067,7 @@ impl ConversationEditor {
|
||||
workspace: workspace.downgrade(),
|
||||
_subscriptions,
|
||||
};
|
||||
this.update_active_buffer(workspace, cx);
|
||||
cx.defer(|this, cx| this.update_active_buffer(workspace, cx));
|
||||
this.update_message_headers(cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -52,12 +52,19 @@ impl Render for Breadcrumbs {
|
||||
Some(BreadcrumbText {
|
||||
text: "⋯".into(),
|
||||
highlights: None,
|
||||
font: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
let mut text_style = cx.text_style();
|
||||
if let Some(font) = segment.font {
|
||||
text_style.font_family = font.family;
|
||||
text_style.font_features = font.features;
|
||||
text_style.font_style = font.style;
|
||||
text_style.font_weight = font.weight;
|
||||
}
|
||||
text_style.color = Color::Muted.color(cx);
|
||||
|
||||
StyledText::new(segment.text.replace('\n', ""))
|
||||
|
||||
@@ -264,7 +264,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx),
|
||||
channel.next_event(cx).await,
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
@@ -317,7 +317,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx),
|
||||
channel.next_event(cx).await,
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
|
||||
@@ -132,7 +132,7 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
move |_: &SignOut, cx| {
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(|cx| async move {
|
||||
client.disconnect(&cx);
|
||||
client.sign_out(&cx).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -1250,6 +1250,15 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.state.write().credentials = None;
|
||||
self.disconnect(&cx);
|
||||
|
||||
if self.has_keychain_credentials(cx).await {
|
||||
delete_credentials_from_keychain(cx).await.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
|
||||
@@ -64,7 +64,7 @@ toml.workspace = true
|
||||
tower = "0.4"
|
||||
tower-http = { workspace = true, features = ["trace"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "tracing-subscriber-0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS "embeddings" (
|
||||
"model" TEXT,
|
||||
"digest" BYTEA,
|
||||
"dimensions" FLOAT4[1536],
|
||||
"retrieved_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY ("model", "digest")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_retrieved_at_on_embeddings" ON "embeddings" ("retrieved_at");
|
||||
@@ -106,8 +106,12 @@ async fn get_extension_versions(
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DownloadLatestExtensionParams {
|
||||
struct DownloadLatestExtensionPathParams {
|
||||
extension_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DownloadLatestExtensionQueryParams {
|
||||
min_schema_version: Option<i32>,
|
||||
max_schema_version: Option<i32>,
|
||||
min_wasm_api_version: Option<SemanticVersion>,
|
||||
@@ -116,13 +120,14 @@ struct DownloadLatestExtensionParams {
|
||||
|
||||
async fn download_latest_extension(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Path(params): Path<DownloadLatestExtensionParams>,
|
||||
Path(params): Path<DownloadLatestExtensionPathParams>,
|
||||
Query(query): Query<DownloadLatestExtensionQueryParams>,
|
||||
) -> Result<Redirect> {
|
||||
let constraints = maybe!({
|
||||
let min_schema_version = params.min_schema_version?;
|
||||
let max_schema_version = params.max_schema_version?;
|
||||
let min_wasm_api_version = params.min_wasm_api_version?;
|
||||
let max_wasm_api_version = params.max_wasm_api_version?;
|
||||
let min_schema_version = query.min_schema_version?;
|
||||
let max_schema_version = query.max_schema_version?;
|
||||
let min_wasm_api_version = query.min_wasm_api_version?;
|
||||
let max_wasm_api_version = query.max_wasm_api_version?;
|
||||
|
||||
Some(ExtensionVersionConstraints {
|
||||
schema_versions: min_schema_version..=max_schema_version,
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod channels;
|
||||
pub mod contacts;
|
||||
pub mod contributors;
|
||||
pub mod dev_servers;
|
||||
pub mod embeddings;
|
||||
pub mod extensions;
|
||||
pub mod hosted_projects;
|
||||
pub mod messages;
|
||||
|
||||
94
crates/collab/src/db/queries/embeddings.rs
Normal file
94
crates/collab/src/db/queries/embeddings.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use super::*;
|
||||
use time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl Database {
|
||||
pub async fn get_embeddings(
|
||||
&self,
|
||||
model: &str,
|
||||
digests: &[Vec<u8>],
|
||||
) -> Result<HashMap<Vec<u8>, Vec<f32>>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
let embeddings = {
|
||||
let mut db_embeddings = embedding::Entity::find()
|
||||
.filter(
|
||||
embedding::Column::Model.eq(model).and(
|
||||
embedding::Column::Digest
|
||||
.is_in(digests.iter().map(|digest| digest.as_slice())),
|
||||
),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut embeddings = HashMap::default();
|
||||
while let Some(db_embedding) = db_embeddings.next().await {
|
||||
let db_embedding = db_embedding?;
|
||||
embeddings.insert(db_embedding.digest, db_embedding.dimensions);
|
||||
}
|
||||
embeddings
|
||||
};
|
||||
|
||||
if !embeddings.is_empty() {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let retrieved_at = PrimitiveDateTime::new(now.date(), now.time());
|
||||
|
||||
embedding::Entity::update_many()
|
||||
.filter(
|
||||
embedding::Column::Digest
|
||||
.is_in(embeddings.keys().map(|digest| digest.as_slice())),
|
||||
)
|
||||
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(embeddings)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_embeddings(
|
||||
&self,
|
||||
model: &str,
|
||||
embeddings: &HashMap<Vec<u8>, Vec<f32>>,
|
||||
) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
embedding::Entity::insert_many(embeddings.iter().map(|(digest, dimensions)| {
|
||||
let now_offset_datetime = OffsetDateTime::now_utc();
|
||||
let retrieved_at =
|
||||
PrimitiveDateTime::new(now_offset_datetime.date(), now_offset_datetime.time());
|
||||
|
||||
embedding::ActiveModel {
|
||||
model: ActiveValue::set(model.to_string()),
|
||||
digest: ActiveValue::set(digest.clone()),
|
||||
dimensions: ActiveValue::set(dimensions.clone()),
|
||||
retrieved_at: ActiveValue::set(retrieved_at),
|
||||
}
|
||||
}))
|
||||
.on_conflict(
|
||||
OnConflict::columns([embedding::Column::Model, embedding::Column::Digest])
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn purge_old_embeddings(&self) -> Result<()> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
embedding::Entity::delete_many()
|
||||
.filter(
|
||||
embedding::Column::RetrievedAt
|
||||
.lte(OffsetDateTime::now_utc() - Duration::days(60)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ pub mod channel_message_mention;
|
||||
pub mod contact;
|
||||
pub mod contributor;
|
||||
pub mod dev_server;
|
||||
pub mod embedding;
|
||||
pub mod extension;
|
||||
pub mod extension_version;
|
||||
pub mod feature_flag;
|
||||
|
||||
18
crates/collab/src/db/tables/embedding.rs
Normal file
18
crates/collab/src/db/tables/embedding.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "embeddings")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub model: String,
|
||||
#[sea_orm(primary_key)]
|
||||
pub digest: Vec<u8>,
|
||||
pub dimensions: Vec<f32>,
|
||||
pub retrieved_at: PrimitiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -2,6 +2,7 @@ mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
mod embedding_tests;
|
||||
mod extension_tests;
|
||||
mod feature_flag_tests;
|
||||
mod message_tests;
|
||||
|
||||
84
crates/collab/src/db/tests/embedding_tests.rs
Normal file
84
crates/collab/src/db/tests/embedding_tests.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use super::TestDb;
|
||||
use crate::db::embedding;
|
||||
use collections::HashMap;
|
||||
use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, QueryFilter};
|
||||
use std::ops::Sub;
|
||||
use time::{Duration, OffsetDateTime, PrimitiveDateTime};
|
||||
|
||||
// SQLite does not support array arguments, so we only test this against a real postgres instance
|
||||
#[gpui::test]
|
||||
async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor().clone());
|
||||
let db = test_db.db();
|
||||
|
||||
let provider = "test_model";
|
||||
let digest1 = vec![1, 2, 3];
|
||||
let digest2 = vec![4, 5, 6];
|
||||
let embeddings = HashMap::from_iter([
|
||||
(digest1.clone(), vec![0.1, 0.2, 0.3]),
|
||||
(digest2.clone(), vec![0.4, 0.5, 0.6]),
|
||||
]);
|
||||
|
||||
// Save embeddings
|
||||
db.save_embeddings(provider, &embeddings).await.unwrap();
|
||||
|
||||
// Retrieve embeddings
|
||||
let retrieved_embeddings = db
|
||||
.get_embeddings(provider, &[digest1.clone(), digest2.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(retrieved_embeddings.len(), 2);
|
||||
assert!(retrieved_embeddings.contains_key(&digest1));
|
||||
assert!(retrieved_embeddings.contains_key(&digest2));
|
||||
|
||||
// Check if the retrieved embeddings are correct
|
||||
assert_eq!(retrieved_embeddings[&digest1], vec![0.1, 0.2, 0.3]);
|
||||
assert_eq!(retrieved_embeddings[&digest2], vec![0.4, 0.5, 0.6]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor().clone());
|
||||
let db = test_db.db();
|
||||
|
||||
let model = "test_model";
|
||||
let digest = vec![7, 8, 9];
|
||||
let embeddings = HashMap::from_iter([(digest.clone(), vec![0.7, 0.8, 0.9])]);
|
||||
|
||||
// Save old embeddings
|
||||
db.save_embeddings(model, &embeddings).await.unwrap();
|
||||
|
||||
// Reach into the DB and change the retrieved at to be > 60 days
|
||||
db.weak_transaction(|tx| {
|
||||
let digest = digest.clone();
|
||||
async move {
|
||||
let sixty_days_ago = OffsetDateTime::now_utc().sub(Duration::days(61));
|
||||
let retrieved_at = PrimitiveDateTime::new(sixty_days_ago.date(), sixty_days_ago.time());
|
||||
|
||||
embedding::Entity::update_many()
|
||||
.filter(
|
||||
embedding::Column::Model
|
||||
.eq(model)
|
||||
.and(embedding::Column::Digest.eq(digest)),
|
||||
)
|
||||
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
|
||||
.exec(&*tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Purge old embeddings
|
||||
db.purge_old_embeddings().await.unwrap();
|
||||
|
||||
// Try to retrieve the purged embeddings
|
||||
let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
|
||||
assert!(
|
||||
retrieved_embeddings.is_empty(),
|
||||
"Old embeddings should have been purged"
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ use axum::{
|
||||
Extension, Router,
|
||||
};
|
||||
use collab::{
|
||||
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
|
||||
Config, RateLimiter, Result,
|
||||
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor,
|
||||
rpc::ResultExt, AppState, Config, RateLimiter, Result,
|
||||
};
|
||||
use db::Database;
|
||||
use std::{
|
||||
@@ -23,7 +23,7 @@ use tower_http::trace::TraceLayer;
|
||||
use tracing_subscriber::{
|
||||
filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt, Layer,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use util::ResultExt as _;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||
@@ -90,6 +90,7 @@ async fn main() -> Result<()> {
|
||||
};
|
||||
|
||||
if is_collab {
|
||||
state.db.purge_old_embeddings().await.trace_err();
|
||||
RateLimiter::save_periodically(state.rate_limiter.clone(), state.executor.clone());
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ use axum::{
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use core::fmt::{self, Debug, Formatter};
|
||||
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
|
||||
use sha2::Digest;
|
||||
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
@@ -568,6 +570,22 @@ impl Server {
|
||||
app_state.config.google_ai_api_key.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.add_request_handler({
|
||||
user_handler(move |request, response, session| {
|
||||
get_cached_embeddings(request, response, session)
|
||||
})
|
||||
})
|
||||
.add_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
user_handler(move |request, response, session| {
|
||||
compute_embeddings(
|
||||
request,
|
||||
response,
|
||||
session,
|
||||
app_state.config.openai_api_key.clone(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
Arc::new(server)
|
||||
@@ -4021,8 +4039,6 @@ async fn complete_with_open_ai(
|
||||
session: UserSession,
|
||||
api_key: Arc<str>,
|
||||
) -> Result<()> {
|
||||
const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
let mut completion_stream = open_ai::stream_completion(
|
||||
&session.http_client,
|
||||
OPEN_AI_API_URL,
|
||||
@@ -4276,6 +4292,128 @@ async fn count_tokens_with_language_model(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ComputeEmbeddingsRateLimit;
|
||||
|
||||
impl RateLimit for ComputeEmbeddingsRateLimit {
|
||||
fn capacity() -> usize {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
chrono::Duration::hours(1)
|
||||
}
|
||||
|
||||
fn db_name() -> &'static str {
|
||||
"compute-embeddings"
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_embeddings(
|
||||
request: proto::ComputeEmbeddings,
|
||||
response: Response<proto::ComputeEmbeddings>,
|
||||
session: UserSession,
|
||||
api_key: Option<Arc<str>>,
|
||||
) -> Result<()> {
|
||||
let api_key = api_key.context("no OpenAI API key configured on the server")?;
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
session
|
||||
.rate_limiter
|
||||
.check::<ComputeEmbeddingsRateLimit>(session.user_id())
|
||||
.await?;
|
||||
|
||||
let embeddings = match request.model.as_str() {
|
||||
"openai/text-embedding-3-small" => {
|
||||
open_ai::embed(
|
||||
&session.http_client,
|
||||
OPEN_AI_API_URL,
|
||||
&api_key,
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
request.texts.iter().map(|text| text.as_str()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
provider => return Err(anyhow!("unsupported embedding provider {:?}", provider))?,
|
||||
};
|
||||
|
||||
let embeddings = request
|
||||
.texts
|
||||
.iter()
|
||||
.map(|text| {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(text.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
result.to_vec()
|
||||
})
|
||||
.zip(
|
||||
embeddings
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|embedding| embedding.embedding),
|
||||
)
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let db = session.db().await;
|
||||
db.save_embeddings(&request.model, &embeddings)
|
||||
.await
|
||||
.context("failed to save embeddings")
|
||||
.trace_err();
|
||||
|
||||
response.send(proto::ComputeEmbeddingsResponse {
|
||||
embeddings: embeddings
|
||||
.into_iter()
|
||||
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
|
||||
.collect(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct GetCachedEmbeddingsRateLimit;
|
||||
|
||||
impl RateLimit for GetCachedEmbeddingsRateLimit {
|
||||
fn capacity() -> usize {
|
||||
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(120) // Picked arbitrarily
|
||||
}
|
||||
|
||||
fn refill_duration() -> chrono::Duration {
|
||||
chrono::Duration::hours(1)
|
||||
}
|
||||
|
||||
fn db_name() -> &'static str {
|
||||
"get-cached-embeddings"
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_embeddings(
|
||||
request: proto::GetCachedEmbeddings,
|
||||
response: Response<proto::GetCachedEmbeddings>,
|
||||
session: UserSession,
|
||||
) -> Result<()> {
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
session
|
||||
.rate_limiter
|
||||
.check::<GetCachedEmbeddingsRateLimit>(session.user_id())
|
||||
.await?;
|
||||
|
||||
let db = session.db().await;
|
||||
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
|
||||
|
||||
response.send(proto::GetCachedEmbeddingsResponse {
|
||||
embeddings: embeddings
|
||||
.into_iter()
|
||||
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
|
||||
.collect(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn authorize_access_to_language_models(session: &UserSession) -> Result<(), Error> {
|
||||
let db = session.db().await;
|
||||
let flags = db.get_user_flags(session.user_id()).await?;
|
||||
|
||||
@@ -31,7 +31,7 @@ impl fmt::Display for ZedVersion {
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn can_collaborate(&self) -> bool {
|
||||
self.0 >= SemanticVersion::new(0, 127, 3)
|
||||
self.0 >= SemanticVersion::new(0, 129, 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ use language::{
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
FakeLspAdapter,
|
||||
};
|
||||
use project::SERVER_PROGRESS_DEBOUNCE_TIMEOUT;
|
||||
use project::{
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
|
||||
};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -1999,6 +2002,25 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
// Turn inline-blame-off by default so no state is transferred without us explicitly doing so
|
||||
let inline_blame_off_settings = Some(InlineBlameSettings {
|
||||
enabled: false,
|
||||
delay_ms: None,
|
||||
});
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||
settings.git.inline_blame = inline_blame_off_settings;
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||
settings.git.inline_blame = inline_blame_off_settings;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
@@ -2100,14 +2122,12 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
|
||||
blame.update(cx, |blame, _| {
|
||||
for (idx, entry) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(entry).unwrap();
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
assert_eq!(
|
||||
blame.permalink_for_entry(entry).unwrap().to_string(),
|
||||
details.permalink.unwrap().to_string(),
|
||||
format!("http://example.com/codehost/idx-{}", idx)
|
||||
);
|
||||
assert_eq!(
|
||||
blame.message_for_entry(entry).unwrap(),
|
||||
format!("message for idx-{}", idx)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1347,13 +1347,11 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
client.username
|
||||
);
|
||||
|
||||
let host_saved_version_fingerprint =
|
||||
host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
|
||||
let guest_saved_version_fingerprint =
|
||||
guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
|
||||
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
|
||||
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
|
||||
assert_eq!(
|
||||
guest_saved_version_fingerprint, host_saved_version_fingerprint,
|
||||
"guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
|
||||
guest_is_dirty, host_is_dirty,
|
||||
"guest {} dirty state does not match host's for path {path:?} in project {project_id}",
|
||||
client.username
|
||||
);
|
||||
|
||||
|
||||
@@ -531,6 +531,8 @@ impl ChatPanel {
|
||||
&self.languages,
|
||||
self.client.id(),
|
||||
&message,
|
||||
self.local_timezone,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
el.child(
|
||||
@@ -744,6 +746,8 @@ impl ChatPanel {
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
current_user_id: u64,
|
||||
message: &channel::ChannelMessage,
|
||||
local_timezone: UtcOffset,
|
||||
cx: &AppContext,
|
||||
) -> RichText {
|
||||
let mentions = message
|
||||
.mentions
|
||||
@@ -754,24 +758,39 @@ impl ChatPanel {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
const MESSAGE_UPDATED: &str = " (edited)";
|
||||
const MESSAGE_EDITED: &str = " (edited)";
|
||||
|
||||
let mut body = message.body.clone();
|
||||
|
||||
if message.edited_at.is_some() {
|
||||
body.push_str(MESSAGE_UPDATED);
|
||||
body.push_str(MESSAGE_EDITED);
|
||||
}
|
||||
|
||||
let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
|
||||
|
||||
if message.edited_at.is_some() {
|
||||
let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
|
||||
rich_text.highlights.push((
|
||||
(rich_text.text.len() - MESSAGE_UPDATED.len())..rich_text.text.len(),
|
||||
range.clone(),
|
||||
Highlight::Highlight(HighlightStyle {
|
||||
fade_out: Some(0.8),
|
||||
color: Some(cx.theme().colors().text_muted),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
|
||||
if let Some(edit_timestamp) = message.edited_at {
|
||||
let edit_timestamp_text = time_format::format_localized_timestamp(
|
||||
edit_timestamp,
|
||||
OffsetDateTime::now_utc(),
|
||||
local_timezone,
|
||||
time_format::TimestampFormat::Absolute,
|
||||
);
|
||||
|
||||
rich_text.custom_ranges.push(range);
|
||||
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| {
|
||||
Some(Tooltip::text(edit_timestamp_text.clone(), cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
rich_text
|
||||
}
|
||||
@@ -1176,7 +1195,13 @@ mod tests {
|
||||
edited_at: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
let message = ChatPanel::render_markdown_with_mentions(
|
||||
&language_registry,
|
||||
102,
|
||||
&message,
|
||||
UtcOffset::UTC,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
|
||||
@@ -1224,7 +1249,13 @@ mod tests {
|
||||
edited_at: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
let message = ChatPanel::render_markdown_with_mentions(
|
||||
&language_registry,
|
||||
102,
|
||||
&message,
|
||||
UtcOffset::UTC,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) =
|
||||
@@ -1265,7 +1296,13 @@ mod tests {
|
||||
edited_at: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
let message = ChatPanel::render_markdown_with_mentions(
|
||||
&language_registry,
|
||||
102,
|
||||
&message,
|
||||
UtcOffset::UTC,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) = marked_text_ranges(
|
||||
|
||||
@@ -557,6 +557,7 @@ mod tests {
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use language::{Language, LanguageConfig};
|
||||
use project::Project;
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
use util::{http::FakeHttpClient, test::marked_text_ranges};
|
||||
@@ -630,6 +631,7 @@ mod tests {
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
|
||||
@@ -1255,7 +1255,6 @@ mod tests {
|
||||
&self,
|
||||
_: BufferId,
|
||||
_: &clock::Global,
|
||||
_: language::RopeFingerprint,
|
||||
_: language::LineEnding,
|
||||
_: Option<std::time::SystemTime>,
|
||||
_: &mut AppContext,
|
||||
|
||||
@@ -44,8 +44,6 @@ use workspace::{
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ProjectDiagnosticsSettings::register(cx);
|
||||
cx.observe_new_views(ProjectDiagnosticsEditor::register)
|
||||
@@ -63,6 +61,7 @@ struct ProjectDiagnosticsEditor {
|
||||
paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
|
||||
current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
|
||||
include_warnings: bool,
|
||||
context: u32,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -116,7 +115,8 @@ impl ProjectDiagnosticsEditor {
|
||||
workspace.register_action(Self::deploy);
|
||||
}
|
||||
|
||||
fn new(
|
||||
fn new_with_context(
|
||||
context: u32,
|
||||
project_handle: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -136,8 +136,15 @@ impl ProjectDiagnosticsEditor {
|
||||
.entry(*language_server_id)
|
||||
.or_default()
|
||||
.insert(path.clone());
|
||||
if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
|
||||
&& !this.is_dirty(cx)
|
||||
|
||||
if this.is_dirty(cx) {
|
||||
return;
|
||||
}
|
||||
let selections = this.editor.read(cx).selections.all::<usize>(cx);
|
||||
if selections.len() < 2
|
||||
&& selections
|
||||
.first()
|
||||
.map_or(true, |selection| selection.end == selection.start)
|
||||
{
|
||||
this.update_excerpts(Some(*language_server_id), cx);
|
||||
}
|
||||
@@ -174,6 +181,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let summary = project.diagnostic_summary(false, cx);
|
||||
let mut this = Self {
|
||||
project: project_handle,
|
||||
context,
|
||||
summary,
|
||||
workspace,
|
||||
excerpts,
|
||||
@@ -193,6 +201,19 @@ impl ProjectDiagnosticsEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn new(
|
||||
project_handle: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self::new_with_context(
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
project_handle,
|
||||
workspace,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
@@ -423,18 +444,16 @@ impl ProjectDiagnosticsEditor {
|
||||
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
|
||||
if let Some((range, start_ix)) = &mut pending_range {
|
||||
if let Some(entry) = resolved_entry.as_ref() {
|
||||
if entry.range.start.row
|
||||
<= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
|
||||
{
|
||||
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
|
||||
range.end = range.end.max(entry.range.end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let excerpt_start =
|
||||
Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
|
||||
Point::new(range.start.row.saturating_sub(self.context), 0);
|
||||
let excerpt_end = snapshot.clip_point(
|
||||
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
|
||||
Point::new(range.end.row + self.context, u32::MAX),
|
||||
Bias::Left,
|
||||
);
|
||||
let excerpt_id = excerpts
|
||||
@@ -1023,7 +1042,12 @@ mod tests {
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let view = window.build_view(cx, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
project.clone(),
|
||||
workspace.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
@@ -1333,7 +1357,12 @@ mod tests {
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let view = window.build_view(cx, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
ProjectDiagnosticsEditor::new_with_context(
|
||||
1,
|
||||
project.clone(),
|
||||
workspace.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Two language servers start updating diagnostics
|
||||
|
||||
@@ -245,6 +245,7 @@ gpui::actions!(
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleInlayHints,
|
||||
ToggleLineNumbers,
|
||||
ToggleSoftWrap,
|
||||
|
||||
@@ -129,6 +129,7 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::ItemHandle;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::Toast;
|
||||
use workspace::{
|
||||
@@ -137,6 +138,7 @@ use workspace::{
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
|
||||
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const MAX_LINE_LEN: usize = 1024;
|
||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||
@@ -154,7 +156,7 @@ pub fn render_parsed_markdown(
|
||||
parsed: &language::ParsedMarkdown,
|
||||
editor_style: &EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> InteractiveText {
|
||||
let code_span_background_color = cx
|
||||
.theme()
|
||||
@@ -462,7 +464,10 @@ pub struct Editor {
|
||||
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
use_autoclose: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
show_git_blame: bool,
|
||||
show_git_blame_gutter: bool,
|
||||
show_git_blame_inline: bool,
|
||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||
git_blame_inline_enabled: bool,
|
||||
blame: Option<Model<GitBlame>>,
|
||||
blame_subscription: Option<Subscription>,
|
||||
custom_context_menu: Option<
|
||||
@@ -471,13 +476,15 @@ pub struct Editor {
|
||||
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
|
||||
>,
|
||||
>,
|
||||
last_bounds: Option<Bounds<Pixels>>,
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditorSnapshot {
|
||||
pub mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
show_git_blame: bool,
|
||||
render_git_blame_gutter: bool,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
is_focused: bool,
|
||||
@@ -1485,6 +1492,8 @@ impl Editor {
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
gutter_hovered: false,
|
||||
pixel_position_of_newest_cursor: None,
|
||||
last_bounds: None,
|
||||
expect_bounds_change: None,
|
||||
gutter_width: Default::default(),
|
||||
style: None,
|
||||
show_cursor_names: false,
|
||||
@@ -1493,7 +1502,10 @@ impl Editor {
|
||||
vim_replace_map: Default::default(),
|
||||
show_inline_completions: mode == EditorMode::Full,
|
||||
custom_context_menu: None,
|
||||
show_git_blame: false,
|
||||
show_git_blame_gutter: false,
|
||||
show_git_blame_inline: false,
|
||||
show_git_blame_inline_delay_task: None,
|
||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
_subscriptions: vec![
|
||||
@@ -1525,6 +1537,11 @@ impl Editor {
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
|
||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||
|
||||
if this.git_blame_inline_enabled {
|
||||
this.git_blame_inline_enabled = true;
|
||||
this.start_git_blame_inline(false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.report_editor_event("open", None, cx);
|
||||
@@ -1641,10 +1658,7 @@ impl Editor {
|
||||
EditorSnapshot {
|
||||
mode: self.mode,
|
||||
show_gutter: self.show_gutter,
|
||||
show_git_blame: self
|
||||
.blame
|
||||
.as_ref()
|
||||
.map_or(false, |blame| blame.read(cx).has_generated_entries()),
|
||||
render_git_blame_gutter: self.render_git_blame_gutter(cx),
|
||||
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(),
|
||||
@@ -1910,6 +1924,9 @@ impl Editor {
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.discard_inline_completion(cx);
|
||||
if self.git_blame_inline_enabled {
|
||||
self.start_inline_blame_timer(cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||
@@ -3727,7 +3744,7 @@ impl Editor {
|
||||
buffer
|
||||
.edited_ranges_for_transaction::<usize>(transaction)
|
||||
.collect(),
|
||||
1,
|
||||
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
),
|
||||
);
|
||||
@@ -3789,6 +3806,22 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
|
||||
fn start_inline_blame_timer(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() {
|
||||
self.show_git_blame_inline = false;
|
||||
|
||||
self.show_git_blame_inline_delay_task = Some(cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor().timer(delay).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.show_git_blame_inline = true;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
if self.pending_rename.is_some() {
|
||||
return None;
|
||||
@@ -7980,7 +8013,7 @@ impl Editor {
|
||||
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
|
||||
location.buffer.clone(),
|
||||
ranges_for_buffer,
|
||||
1,
|
||||
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
@@ -7998,11 +8031,16 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let item = Box::new(editor);
|
||||
if split {
|
||||
workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
|
||||
workspace.split_item(SplitDirection::Right, item.clone(), cx);
|
||||
} else {
|
||||
workspace.add_item_to_active_pane(Box::new(editor), cx);
|
||||
workspace.add_item_to_active_pane(item.clone(), cx);
|
||||
}
|
||||
workspace.active_pane().clone().update(cx, |pane, cx| {
|
||||
let item_id = item.item_id();
|
||||
pane.set_preview_item_id(Some(item_id), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||
@@ -8833,40 +8871,89 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext<Self>) {
|
||||
if self.show_git_blame {
|
||||
self.blame_subscription.take();
|
||||
self.blame.take();
|
||||
self.show_git_blame = false
|
||||
} else {
|
||||
if let Err(error) = self.show_git_blame_internal(cx) {
|
||||
log::error!("failed to toggle on 'git blame': {}", error);
|
||||
return;
|
||||
}
|
||||
self.show_git_blame = true
|
||||
self.show_git_blame_gutter = !self.show_git_blame_gutter;
|
||||
|
||||
if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
|
||||
self.start_git_blame(true, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_git_blame_internal(&mut self, cx: &mut ViewContext<Self>) -> Result<()> {
|
||||
pub fn toggle_git_blame_inline(
|
||||
&mut self,
|
||||
_: &ToggleGitBlameInline,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.toggle_git_blame_inline_internal(true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn git_blame_inline_enabled(&self) -> bool {
|
||||
self.git_blame_inline_enabled
|
||||
}
|
||||
|
||||
fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
|
||||
if let Some(project) = self.project.as_ref() {
|
||||
let Some(buffer) = self.buffer().read(cx).as_singleton() else {
|
||||
anyhow::bail!("git blame not available in multi buffers")
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.clone();
|
||||
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, cx));
|
||||
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, cx));
|
||||
self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify()));
|
||||
self.blame = Some(blame);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn toggle_git_blame_inline_internal(
|
||||
&mut self,
|
||||
user_triggered: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.git_blame_inline_enabled {
|
||||
self.git_blame_inline_enabled = false;
|
||||
self.show_git_blame_inline = false;
|
||||
self.show_git_blame_inline_delay_task.take();
|
||||
} else {
|
||||
self.git_blame_inline_enabled = true;
|
||||
self.start_git_blame_inline(user_triggered, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn start_git_blame_inline(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
|
||||
self.start_git_blame(user_triggered, cx);
|
||||
|
||||
if ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame_delay()
|
||||
.is_some()
|
||||
{
|
||||
self.start_inline_blame_timer(cx);
|
||||
} else {
|
||||
self.show_git_blame_inline = true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame(&self) -> Option<&Model<GitBlame>> {
|
||||
self.blame.as_ref()
|
||||
}
|
||||
|
||||
pub fn render_git_blame_gutter(&mut self, cx: &mut WindowContext) -> bool {
|
||||
self.show_git_blame_gutter && self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool {
|
||||
self.focus_handle.is_focused(cx) && self.show_git_blame_inline && self.has_blame_entries(cx)
|
||||
}
|
||||
|
||||
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
|
||||
self.blame()
|
||||
.map_or(false, |blame| blame.read(cx).has_generated_entries())
|
||||
}
|
||||
|
||||
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
|
||||
let (path, repo) = maybe!({
|
||||
let project_handle = self.project.as_ref()?.clone();
|
||||
@@ -9436,6 +9523,14 @@ impl Editor {
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
if self.git_blame_inline_enabled != inline_blame_enabled {
|
||||
self.toggle_git_blame_inline_internal(false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -9517,7 +9612,11 @@ impl Editor {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?;
|
||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_path(path, None, true, cx)
|
||||
// Reset the preview item id before opening the new item
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.set_preview_item_id(None, cx);
|
||||
});
|
||||
workspace.open_path_preview(path, None, true, true, cx)
|
||||
})?;
|
||||
let editor = editor
|
||||
.await?
|
||||
@@ -10044,7 +10143,7 @@ impl EditorSnapshot {
|
||||
};
|
||||
|
||||
let git_blame_entries_width = self
|
||||
.show_git_blame
|
||||
.render_git_blame_gutter
|
||||
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
|
||||
|
||||
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
|
||||
@@ -10579,6 +10678,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
||||
text_style.font_style = theme_settings.buffer_font.style;
|
||||
text_style.font_features = theme_settings.buffer_font.features;
|
||||
text_style.font_weight = theme_settings.buffer_font.weight;
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct EditorSettings {
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub scroll_sensitivity: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
pub multi_cursor_modifier: MultiCursorModifier,
|
||||
@@ -57,8 +58,8 @@ pub struct Toolbar {
|
||||
pub struct Scrollbar {
|
||||
pub show: ShowScrollbar,
|
||||
pub git_diff: bool,
|
||||
pub selections: bool,
|
||||
pub symbols_selections: bool,
|
||||
pub selected_symbol: bool,
|
||||
pub search_results: bool,
|
||||
pub diagnostics: bool,
|
||||
}
|
||||
|
||||
@@ -138,6 +139,11 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: 3.
|
||||
pub vertical_scroll_margin: Option<f32>,
|
||||
/// Scroll sensitivity multiplier. This multiplier is applied
|
||||
/// to both the horizontal and vertical delta values while scrolling.
|
||||
///
|
||||
/// Default: 1.0
|
||||
pub scroll_sensitivity: Option<f32>,
|
||||
/// Whether the line numbers on editors gutter are relative or not.
|
||||
///
|
||||
/// Default: false
|
||||
@@ -178,7 +184,7 @@ pub struct ToolbarContent {
|
||||
}
|
||||
|
||||
/// Scrollbar related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct ScrollbarContent {
|
||||
/// When to show the scrollbar in the editor.
|
||||
///
|
||||
@@ -188,14 +194,14 @@ pub struct ScrollbarContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub git_diff: Option<bool>,
|
||||
/// Whether to show buffer search result markers in the scrollbar.
|
||||
/// Whether to show buffer search result indicators in the scrollbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub selections: Option<bool>,
|
||||
/// Whether to show symbols highlighted markers in the scrollbar.
|
||||
pub search_results: Option<bool>,
|
||||
/// Whether to show selected symbol occurrences in the scrollbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub symbols_selections: Option<bool>,
|
||||
pub selected_symbol: Option<bool>,
|
||||
/// Whether to show diagnostic indicators in the scrollbar.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -4,7 +4,10 @@ use crate::{
|
||||
TransformBlock,
|
||||
},
|
||||
editor_settings::{DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar},
|
||||
git::{blame::GitBlame, diff_hunk_to_display, DisplayDiffHunk},
|
||||
git::{
|
||||
blame::{CommitDetails, GitBlame},
|
||||
diff_hunk_to_display, DisplayDiffHunk,
|
||||
},
|
||||
hover_popover::{
|
||||
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
},
|
||||
@@ -21,13 +24,13 @@ use collections::{BTreeMap, HashMap};
|
||||
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AnyView, AvailableSpace, Bounds,
|
||||
ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
|
||||
ElementContext, ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementContext,
|
||||
ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
ViewContext, WindowContext,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle,
|
||||
TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
@@ -49,11 +52,11 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use theme::{ActiveTheme, PlayerColor};
|
||||
use theme::{ActiveTheme, PlayerColor, ThemeSettings};
|
||||
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
|
||||
use ui::{prelude::*, tooltip_container};
|
||||
use util::ResultExt;
|
||||
use workspace::item::Item;
|
||||
use workspace::{item::Item, Workspace};
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
@@ -303,6 +306,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::copy_permalink_to_line);
|
||||
register_action(view, cx, Editor::open_permalink_to_line);
|
||||
register_action(view, cx, Editor::toggle_git_blame);
|
||||
register_action(view, cx, Editor::toggle_git_blame_inline);
|
||||
register_action(view, cx, |editor, action, cx| {
|
||||
if let Some(task) = editor.format(action, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
@@ -961,11 +965,11 @@ impl EditorElement {
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
|
||||
||
|
||||
// Selections
|
||||
(is_singleton && scrollbar_settings.selections && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
||
|
||||
// Symbols Selections
|
||||
(is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
|
||||
// Selected Symbol Occurrences
|
||||
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
|
||||
||
|
||||
// Diagnostics
|
||||
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
|
||||
@@ -1092,6 +1096,63 @@ impl EditorElement {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_blame(
|
||||
&self,
|
||||
display_row: u32,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
line_layout: &LineWithInvisibles,
|
||||
em_width: Pixels,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<AnyElement> {
|
||||
if !self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.render_git_blame_inline(cx))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.workspace
|
||||
.as_ref()
|
||||
.map(|(w, _)| w.clone());
|
||||
|
||||
let display_point = DisplayPoint::new(display_row, 0);
|
||||
let buffer_row = display_point.to_point(display_snapshot).row;
|
||||
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows([Some(buffer_row)], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
|
||||
let mut element =
|
||||
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
|
||||
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row as f32 - scroll_pixel_position.y / line_height);
|
||||
|
||||
let start_x = {
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
|
||||
|
||||
let line_width = line_layout.line.width;
|
||||
content_origin.x + line_width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS)
|
||||
};
|
||||
|
||||
let absolute_offset = point(start_x, start_y);
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
|
||||
element.layout(absolute_offset, available_space, cx);
|
||||
|
||||
Some(element)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_blame_entries(
|
||||
&self,
|
||||
@@ -1103,10 +1164,14 @@ impl EditorElement {
|
||||
max_width: Option<Pixels>,
|
||||
cx: &mut ElementContext,
|
||||
) -> Option<Vec<AnyElement>> {
|
||||
let Some(blame) = self.editor.read(cx).blame.as_ref().cloned() else {
|
||||
if !self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
|
||||
{
|
||||
return None;
|
||||
};
|
||||
}
|
||||
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows(buffer_rows, cx).collect()
|
||||
});
|
||||
@@ -1120,7 +1185,6 @@ impl EditorElement {
|
||||
let start_x = em_width * 1;
|
||||
|
||||
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
|
||||
let text_style = &self.style.text;
|
||||
|
||||
let shaped_lines = blamed_rows
|
||||
.into_iter()
|
||||
@@ -1131,7 +1195,7 @@ impl EditorElement {
|
||||
ix,
|
||||
&blame,
|
||||
blame_entry,
|
||||
text_style,
|
||||
&self.style,
|
||||
&mut last_used_color,
|
||||
self.editor.clone(),
|
||||
cx,
|
||||
@@ -2256,6 +2320,7 @@ impl EditorElement {
|
||||
self.paint_lines(&invisible_display_ranges, layout, cx);
|
||||
self.paint_redactions(layout, cx);
|
||||
self.paint_cursors(layout, cx);
|
||||
self.paint_inline_blame(layout, cx);
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2559,10 +2624,14 @@ impl EditorElement {
|
||||
for (background_highlight_id, (_, background_ranges)) in
|
||||
background_highlights.iter()
|
||||
{
|
||||
if (*background_highlight_id
|
||||
== TypeId::of::<BufferSearchHighlights>()
|
||||
&& scrollbar_settings.selections)
|
||||
|| scrollbar_settings.symbols_selections
|
||||
let is_search_highlights = *background_highlight_id
|
||||
== TypeId::of::<BufferSearchHighlights>();
|
||||
let is_symbol_occurrences = *background_highlight_id
|
||||
== TypeId::of::<DocumentHighlightRead>()
|
||||
|| *background_highlight_id
|
||||
== TypeId::of::<DocumentHighlightWrite>();
|
||||
if (is_search_highlights && scrollbar_settings.search_results)
|
||||
|| (is_symbol_occurrences && scrollbar_settings.selected_symbol)
|
||||
{
|
||||
let marker_row_ranges =
|
||||
background_ranges.into_iter().map(|range| {
|
||||
@@ -2730,6 +2799,14 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) {
|
||||
if let Some(mut inline_blame) = layout.inline_blame.take() {
|
||||
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
|
||||
inline_blame.paint(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) {
|
||||
for mut block in layout.blocks.drain(..) {
|
||||
block.element.paint(cx);
|
||||
@@ -2749,6 +2826,10 @@ impl EditorElement {
|
||||
let hitbox = layout.hitbox.clone();
|
||||
let mut delta = ScrollDelta::default();
|
||||
|
||||
// Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
|
||||
// accidentally turn off their scrolling.
|
||||
let scroll_sensitivity = EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
delta = delta.coalesce(event.delta);
|
||||
@@ -2773,8 +2854,11 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
let scroll_position = position_map.snapshot.scroll_position();
|
||||
let x = (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width;
|
||||
let y = (scroll_position.y * line_height - delta.y) / line_height;
|
||||
let x = (scroll_position.x * max_glyph_width
|
||||
- (delta.x * scroll_sensitivity))
|
||||
/ max_glyph_width;
|
||||
let y = (scroll_position.y * line_height - (delta.y * scroll_sensitivity))
|
||||
/ line_height;
|
||||
let scroll_position =
|
||||
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
|
||||
editor.scroll(scroll_position, axis, cx);
|
||||
@@ -2894,11 +2978,194 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
blame: &gpui::Model<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ElementContext<'_>,
|
||||
) -> AnyElement {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
|
||||
|
||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||
let text = format!("{}, {}", author, relative_timestamp);
|
||||
|
||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
||||
|
||||
let tooltip = cx.new_view(|_| BlameEntryTooltip::new(blame_entry, details, style, workspace));
|
||||
|
||||
h_flex()
|
||||
.id("inline-blame")
|
||||
.w_full()
|
||||
.font(style.text.font().family)
|
||||
.text_color(cx.theme().status().hint)
|
||||
.line_height(style.text.line_height)
|
||||
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
||||
.child(text)
|
||||
.gap_2()
|
||||
.hoverable_tooltip(move |_| tooltip.clone().into())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn blame_entry_timestamp(
|
||||
blame_entry: &BlameEntry,
|
||||
format: time_format::TimestampFormat,
|
||||
cx: &WindowContext,
|
||||
) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
cx.local_timezone(),
|
||||
format,
|
||||
),
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
|
||||
}
|
||||
|
||||
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
|
||||
blame_entry_timestamp(
|
||||
blame_entry,
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
struct BlameEntryTooltip {
|
||||
blame_entry: BlameEntry,
|
||||
details: Option<CommitDetails>,
|
||||
style: EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl BlameEntryTooltip {
|
||||
fn new(
|
||||
blame_entry: BlameEntry,
|
||||
details: Option<CommitDetails>,
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
style: style.clone(),
|
||||
blame_entry,
|
||||
details,
|
||||
workspace,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BlameEntryTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let author = self
|
||||
.blame_entry
|
||||
.author
|
||||
.clone()
|
||||
.unwrap_or("<no name>".to_string());
|
||||
|
||||
let author_email = self.blame_entry.author_mail.clone();
|
||||
|
||||
let pretty_commit_id = format!("{}", self.blame_entry.sha);
|
||||
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
|
||||
|
||||
let message = self
|
||||
.details
|
||||
.as_ref()
|
||||
.map(|details| {
|
||||
crate::render_parsed_markdown(
|
||||
"blame-message",
|
||||
&details.parsed_message,
|
||||
&self.style,
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or("<no commit message>".into_any());
|
||||
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
||||
let message_max_height = cx.line_height() * 12 + (ui_font_size / 0.4);
|
||||
|
||||
tooltip_container(cx, move |this, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.w(gpui::rems(30.))
|
||||
.gap_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_x_2()
|
||||
.overflow_x_hidden()
|
||||
.flex_wrap()
|
||||
.child(author)
|
||||
.when_some(author_email, |this, author_email| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(author_email),
|
||||
)
|
||||
})
|
||||
.pb_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("inline-blame-commit-message")
|
||||
.occlude()
|
||||
.child(message)
|
||||
.max_h(message_max_height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(absolute_timestamp)
|
||||
.child(
|
||||
Button::new("commit-sha-button", short_commit_id.clone())
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::FileGit)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(
|
||||
self.details.as_ref().map_or(true, |details| {
|
||||
details.permalink.is_none()
|
||||
}),
|
||||
)
|
||||
.when_some(
|
||||
self.details
|
||||
.as_ref()
|
||||
.and_then(|details| details.permalink.clone()),
|
||||
|this, url| {
|
||||
this.on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn render_blame_entry(
|
||||
ix: usize,
|
||||
blame: &gpui::Model<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
text_style: &TextStyle,
|
||||
style: &EditorStyle,
|
||||
last_used_color: &mut Option<(PlayerColor, Oid)>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ElementContext<'_>,
|
||||
@@ -2918,29 +3185,26 @@ fn render_blame_entry(
|
||||
};
|
||||
last_used_color.replace((sha_color, blame_entry.sha));
|
||||
|
||||
let relative_timestamp = match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
cx.local_timezone(),
|
||||
time_format::TimestampFormat::Relative,
|
||||
),
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
};
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
|
||||
|
||||
let pretty_commit_id = format!("{}", blame_entry.sha);
|
||||
let short_commit_id = pretty_commit_id.clone().chars().take(6).collect::<String>();
|
||||
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
|
||||
|
||||
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
||||
let name = util::truncate_and_trailoff(author_name, 20);
|
||||
|
||||
let permalink = blame.read(cx).permalink_for_entry(&blame_entry);
|
||||
let commit_message = blame.read(cx).message_for_entry(&blame_entry);
|
||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
||||
|
||||
let workspace = editor.read(cx).workspace.as_ref().map(|(w, _)| w.clone());
|
||||
|
||||
let tooltip = cx.new_view(|_| {
|
||||
BlameEntryTooltip::new(blame_entry.clone(), details.clone(), style, workspace)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.font(text_style.font().family)
|
||||
.line_height(text_style.line_height)
|
||||
.font(style.text.font().family)
|
||||
.line_height(style.text.line_height)
|
||||
.id(("blame", ix))
|
||||
.children([
|
||||
div()
|
||||
@@ -2962,21 +3226,17 @@ fn render_blame_entry(
|
||||
}
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.when_some(permalink, |this, url| {
|
||||
let url = url.clone();
|
||||
this.cursor_pointer().on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
BlameEntryTooltip::new(
|
||||
sha_color.cursor,
|
||||
commit_message.clone(),
|
||||
blame_entry.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.when_some(
|
||||
details.and_then(|details| details.permalink),
|
||||
|this, url| {
|
||||
let url = url.clone();
|
||||
this.cursor_pointer().on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
)
|
||||
.hoverable_tooltip(move |_| tooltip.clone().into())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -2999,84 +3259,6 @@ fn deploy_blame_entry_context_menu(
|
||||
});
|
||||
}
|
||||
|
||||
struct BlameEntryTooltip {
|
||||
color: Hsla,
|
||||
commit_message: Option<String>,
|
||||
blame_entry: BlameEntry,
|
||||
}
|
||||
|
||||
impl BlameEntryTooltip {
|
||||
fn new(
|
||||
color: Hsla,
|
||||
commit_message: Option<String>,
|
||||
blame_entry: BlameEntry,
|
||||
cx: &mut WindowContext,
|
||||
) -> AnyView {
|
||||
cx.new_view(|_cx| Self {
|
||||
color,
|
||||
commit_message,
|
||||
blame_entry,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BlameEntryTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let author = self
|
||||
.blame_entry
|
||||
.author
|
||||
.clone()
|
||||
.unwrap_or("<no name>".to_string());
|
||||
let author_email = self.blame_entry.author_mail.clone().unwrap_or_default();
|
||||
let absolute_timestamp = match self.blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
cx.local_timezone(),
|
||||
time_format::TimestampFormat::Absolute,
|
||||
),
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
};
|
||||
|
||||
let message = match &self.commit_message {
|
||||
Some(message) => util::truncate_lines_and_trailoff(message, 15),
|
||||
None => self.blame_entry.summary.clone().unwrap_or_default(),
|
||||
};
|
||||
|
||||
let pretty_commit_id = format!("{}", self.blame_entry.sha);
|
||||
|
||||
tooltip_container(cx, move |this, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Commit")
|
||||
.pr_2(),
|
||||
)
|
||||
.child(
|
||||
div().text_color(self.color).child(pretty_commit_id.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.child(format!(
|
||||
"{} {} - {}",
|
||||
author, author_email, absolute_timestamp
|
||||
))
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
.child(div().child(message)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineWithInvisibles {
|
||||
pub line: ShapedLine,
|
||||
@@ -3205,13 +3387,9 @@ impl LineWithInvisibles {
|
||||
let line_y =
|
||||
line_height * (row as f32 - layout.position_map.scroll_pixel_position.y / line_height);
|
||||
|
||||
self.line
|
||||
.paint(
|
||||
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y),
|
||||
line_height,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
let line_origin =
|
||||
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
|
||||
self.line.paint(line_origin, line_height, cx).log_err();
|
||||
|
||||
self.draw_invisibles(
|
||||
&selection_ranges,
|
||||
@@ -3371,6 +3549,7 @@ impl Element for EditorElement {
|
||||
let overscroll = size(em_width, px(0.));
|
||||
|
||||
snapshot = self.editor.update(cx, |editor, cx| {
|
||||
editor.last_bounds = Some(bounds);
|
||||
editor.gutter_width = gutter_dimensions.width;
|
||||
editor.set_visible_line_count(bounds.size.height / line_height, cx);
|
||||
|
||||
@@ -3419,7 +3598,7 @@ impl Element for EditorElement {
|
||||
|
||||
let autoscroll_horizontally = self.editor.update(cx, |editor, cx| {
|
||||
let autoscroll_horizontally =
|
||||
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
|
||||
editor.autoscroll_vertically(bounds, line_height, cx);
|
||||
snapshot = editor.snapshot(cx);
|
||||
autoscroll_horizontally
|
||||
});
|
||||
@@ -3489,16 +3668,6 @@ impl Element for EditorElement {
|
||||
|
||||
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
|
||||
|
||||
let blamed_display_rows = self.layout_blame_entries(
|
||||
buffer_rows,
|
||||
em_width,
|
||||
scroll_position,
|
||||
line_height,
|
||||
&gutter_hitbox,
|
||||
gutter_dimensions.git_blame_entries_width,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut max_visible_line_width = Pixels::ZERO;
|
||||
let line_layouts =
|
||||
self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
|
||||
@@ -3527,6 +3696,39 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let mut inline_blame = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let display_row = newest_selection_head.row();
|
||||
if (start_row..end_row).contains(&display_row) {
|
||||
let line_layout = &line_layouts[(display_row - start_row) as usize];
|
||||
inline_blame = self.layout_inline_blame(
|
||||
display_row,
|
||||
&snapshot.display_snapshot,
|
||||
line_layout,
|
||||
em_width,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
line_height,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let blamed_display_rows = self.layout_blame_entries(
|
||||
buffer_rows,
|
||||
em_width,
|
||||
scroll_position,
|
||||
line_height,
|
||||
&gutter_hitbox,
|
||||
gutter_dimensions.git_blame_entries_width,
|
||||
cx,
|
||||
);
|
||||
|
||||
let scroll_max = point(
|
||||
((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
|
||||
max_row as f32,
|
||||
@@ -3554,11 +3756,6 @@ impl Element for EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
cx.with_element_id(Some("blocks"), |cx| {
|
||||
self.layout_blocks(
|
||||
&mut blocks,
|
||||
@@ -3727,6 +3924,7 @@ impl Element for EditorElement {
|
||||
line_numbers,
|
||||
display_hunks,
|
||||
blamed_display_rows,
|
||||
inline_blame,
|
||||
folds,
|
||||
blocks,
|
||||
cursors,
|
||||
@@ -3814,6 +4012,7 @@ pub struct EditorLayout {
|
||||
line_numbers: Vec<Option<ShapedLine>>,
|
||||
display_hunks: Vec<DisplayDiffHunk>,
|
||||
blamed_display_rows: Option<Vec<AnyElement>>,
|
||||
inline_blame: Option<AnyElement>,
|
||||
folds: Vec<FoldLayout>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git::{
|
||||
@@ -5,7 +7,7 @@ use git::{
|
||||
Oid,
|
||||
};
|
||||
use gpui::{Model, ModelContext, Subscription, Task};
|
||||
use language::{Bias, Buffer, BufferSnapshot, Edit};
|
||||
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
|
||||
use project::{Item, Project};
|
||||
use smallvec::SmallVec;
|
||||
use sum_tree::SumTree;
|
||||
@@ -44,23 +46,32 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommitDetails {
|
||||
pub message: String,
|
||||
pub parsed_message: ParsedMarkdown,
|
||||
pub permalink: Option<Url>,
|
||||
}
|
||||
|
||||
pub struct GitBlame {
|
||||
project: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
entries: SumTree<GitBlameEntry>,
|
||||
permalinks: HashMap<Oid, Url>,
|
||||
messages: HashMap<Oid, String>,
|
||||
commit_details: HashMap<Oid, CommitDetails>,
|
||||
buffer_snapshot: BufferSnapshot,
|
||||
buffer_edits: text::Subscription,
|
||||
task: Task<Result<()>>,
|
||||
generated: bool,
|
||||
_refresh_subscription: Subscription,
|
||||
user_triggered: bool,
|
||||
regenerate_on_edit_task: Task<Result<()>>,
|
||||
_regenerate_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl GitBlame {
|
||||
pub fn new(
|
||||
buffer: Model<Buffer>,
|
||||
project: Model<Project>,
|
||||
user_triggered: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let entries = SumTree::from_item(
|
||||
@@ -71,7 +82,19 @@ impl GitBlame {
|
||||
&(),
|
||||
);
|
||||
|
||||
let refresh_subscription = cx.subscribe(&project, {
|
||||
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
|
||||
language::Event::DirtyChanged => {
|
||||
if !buffer.read(cx).is_dirty() {
|
||||
this.generate(cx);
|
||||
}
|
||||
}
|
||||
language::Event::Edited => {
|
||||
this.regenerate_on_edit(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let project_subscription = cx.subscribe(&project, {
|
||||
let buffer = buffer.clone();
|
||||
|
||||
move |this, _, event, cx| match event {
|
||||
@@ -102,11 +125,12 @@ impl GitBlame {
|
||||
buffer_snapshot,
|
||||
entries,
|
||||
buffer_edits,
|
||||
permalinks: HashMap::default(),
|
||||
messages: HashMap::default(),
|
||||
user_triggered,
|
||||
commit_details: HashMap::default(),
|
||||
task: Task::ready(Ok(())),
|
||||
generated: false,
|
||||
_refresh_subscription: refresh_subscription,
|
||||
regenerate_on_edit_task: Task::ready(Ok(())),
|
||||
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
|
||||
};
|
||||
this.generate(cx);
|
||||
this
|
||||
@@ -116,12 +140,8 @@ impl GitBlame {
|
||||
self.generated
|
||||
}
|
||||
|
||||
pub fn permalink_for_entry(&self, entry: &BlameEntry) -> Option<Url> {
|
||||
self.permalinks.get(&entry.sha).cloned()
|
||||
}
|
||||
|
||||
pub fn message_for_entry(&self, entry: &BlameEntry) -> Option<String> {
|
||||
self.messages.get(&entry.sha).cloned()
|
||||
pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<CommitDetails> {
|
||||
self.commit_details.get(&entry.sha).cloned()
|
||||
}
|
||||
|
||||
pub fn blame_for_rows<'a>(
|
||||
@@ -254,9 +274,10 @@ impl GitBlame {
|
||||
let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let snapshot = self.buffer.read(cx).snapshot();
|
||||
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
|
||||
self.task = cx.spawn(|this, mut cx| async move {
|
||||
let (entries, permalinks, messages) = cx
|
||||
let result = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let snapshot = snapshot.clone();
|
||||
@@ -267,56 +288,133 @@ impl GitBlame {
|
||||
messages,
|
||||
} = blame.await?;
|
||||
|
||||
let mut current_row = 0;
|
||||
let mut entries = SumTree::from_iter(
|
||||
entries.into_iter().flat_map(|entry| {
|
||||
let mut entries = SmallVec::<[GitBlameEntry; 2]>::new();
|
||||
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
|
||||
let commit_details =
|
||||
parse_commit_messages(messages, &permalinks, &languages).await;
|
||||
|
||||
if entry.range.start > current_row {
|
||||
let skipped_rows = entry.range.start - current_row;
|
||||
entries.push(GitBlameEntry {
|
||||
rows: skipped_rows,
|
||||
blame: None,
|
||||
});
|
||||
}
|
||||
entries.push(GitBlameEntry {
|
||||
rows: entry.range.len() as u32,
|
||||
blame: Some(entry.clone()),
|
||||
});
|
||||
|
||||
current_row = entry.range.end;
|
||||
entries
|
||||
}),
|
||||
&(),
|
||||
);
|
||||
|
||||
let max_row = snapshot.max_point().row;
|
||||
if max_row >= current_row {
|
||||
entries.push(
|
||||
GitBlameEntry {
|
||||
rows: (max_row + 1) - current_row,
|
||||
blame: None,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
}
|
||||
|
||||
anyhow::Ok((entries, permalinks, messages))
|
||||
anyhow::Ok((entries, commit_details))
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.buffer_edits = buffer_edits;
|
||||
this.buffer_snapshot = snapshot;
|
||||
this.entries = entries;
|
||||
this.permalinks = permalinks;
|
||||
this.messages = messages;
|
||||
this.generated = true;
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, cx| match result {
|
||||
Ok((entries, commit_details)) => {
|
||||
this.buffer_edits = buffer_edits;
|
||||
this.buffer_snapshot = snapshot;
|
||||
this.entries = entries;
|
||||
this.commit_details = commit_details;
|
||||
this.generated = true;
|
||||
cx.notify();
|
||||
}
|
||||
Err(error) => this.project.update(cx, |_, cx| {
|
||||
if this.user_triggered {
|
||||
log::error!("failed to get git blame data: {error:?}");
|
||||
let notification = format!("{:#}", error).trim().to_string();
|
||||
cx.emit(project::Event::Notification(notification));
|
||||
} else {
|
||||
// If we weren't triggered by a user, we just log errors in the background, instead of sending
|
||||
// notifications.
|
||||
// Except for `NoRepositoryError`, which can happen often if a user has inline-blame turned on
|
||||
// and opens a non-git file.
|
||||
if error.downcast_ref::<project::NoRepositoryError>().is_none() {
|
||||
log::error!("failed to get git blame data: {error:?}");
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn regenerate_on_edit(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.regenerate_on_edit_task = cx.spawn(|this, mut cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.generate(cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree<GitBlameEntry> {
|
||||
let mut current_row = 0;
|
||||
let mut entries = SumTree::from_iter(
|
||||
entries.into_iter().flat_map(|entry| {
|
||||
let mut entries = SmallVec::<[GitBlameEntry; 2]>::new();
|
||||
|
||||
if entry.range.start > current_row {
|
||||
let skipped_rows = entry.range.start - current_row;
|
||||
entries.push(GitBlameEntry {
|
||||
rows: skipped_rows,
|
||||
blame: None,
|
||||
});
|
||||
}
|
||||
entries.push(GitBlameEntry {
|
||||
rows: entry.range.len() as u32,
|
||||
blame: Some(entry.clone()),
|
||||
});
|
||||
|
||||
current_row = entry.range.end;
|
||||
entries
|
||||
}),
|
||||
&(),
|
||||
);
|
||||
|
||||
if max_row >= current_row {
|
||||
entries.push(
|
||||
GitBlameEntry {
|
||||
rows: (max_row + 1) - current_row,
|
||||
blame: None,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
permalinks: &HashMap<Oid, Url>,
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
) -> HashMap<Oid, CommitDetails> {
|
||||
let mut commit_details = HashMap::default();
|
||||
for (oid, message) in messages {
|
||||
let parsed_message = parse_markdown(&message, &languages).await;
|
||||
let permalink = permalinks.get(&oid).cloned();
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
CommitDetails {
|
||||
message,
|
||||
parsed_message,
|
||||
permalink,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
commit_details
|
||||
}
|
||||
|
||||
async fn parse_markdown(text: &str, language_registry: &Arc<LanguageRegistry>) -> ParsedMarkdown {
|
||||
let mut parsed_message = ParsedMarkdown::default();
|
||||
|
||||
markdown::parse_markdown_block(
|
||||
text,
|
||||
language_registry,
|
||||
None,
|
||||
&mut parsed_message.text,
|
||||
&mut parsed_message.highlights,
|
||||
&mut parsed_message.region_ranges,
|
||||
&mut parsed_message.regions,
|
||||
)
|
||||
.await;
|
||||
|
||||
parsed_message
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -359,6 +457,54 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_blame_error_notifications(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/my-repo",
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": r#"
|
||||
irrelevant contents
|
||||
"#
|
||||
.unindent()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Creating a GitBlame without a corresponding blame state
|
||||
// will result in an error.
|
||||
|
||||
let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/my-repo/file.txt", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project.clone(), true, cx));
|
||||
|
||||
let event = project.next_event(cx).await;
|
||||
assert_eq!(
|
||||
event,
|
||||
project::Event::Notification(
|
||||
"Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string()
|
||||
)
|
||||
);
|
||||
|
||||
blame.update(cx, |blame, cx| {
|
||||
assert_eq!(
|
||||
blame
|
||||
.blame_for_rows((0..1).map(Some), cx)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![None]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -408,7 +554,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -488,7 +634,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -637,7 +783,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
|
||||
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
|
||||
cx.executor().run_until_parked();
|
||||
git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use text::{BufferId, Selection};
|
||||
use theme::Theme;
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, Label};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
|
||||
@@ -735,9 +735,8 @@ impl Item for Editor {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
let version = buffer.saved_version().clone();
|
||||
let fingerprint = buffer.saved_version_fingerprint();
|
||||
let mtime = buffer.saved_mtime();
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
buffer.did_save(version, mtime, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -828,13 +827,18 @@ impl Item for Editor {
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut breadcrumbs = vec![BreadcrumbText {
|
||||
text: filename,
|
||||
highlights: None,
|
||||
font: Some(settings.buffer_font.clone()),
|
||||
}];
|
||||
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
|
||||
text: symbol.text,
|
||||
highlights: Some(symbol.highlight_ranges),
|
||||
font: Some(settings.buffer_font.clone()),
|
||||
}));
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
@@ -1167,6 +1171,10 @@ impl SearchableItem for Editor {
|
||||
&self.buffer().read(cx).snapshot(cx),
|
||||
)
|
||||
}
|
||||
|
||||
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
|
||||
self.expect_bounds_change = self.last_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_match_index(
|
||||
|
||||
@@ -47,6 +47,12 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
} else if point.column() == 0 {
|
||||
// If the current sofr_wrap mode is used, the column corresponding to the display is 0,
|
||||
// which does not necessarily mean that the actual beginning of a paragraph
|
||||
if map.display_point_to_fold_point(point, Bias::Left).column() > 0 {
|
||||
return left(map, point);
|
||||
}
|
||||
}
|
||||
map.clip_point(point, Bias::Left)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cmp, f32};
|
||||
|
||||
use gpui::{px, Pixels, ViewContext};
|
||||
use gpui::{px, Bounds, Pixels, ViewContext};
|
||||
use language::Point;
|
||||
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
|
||||
@@ -63,13 +63,23 @@ impl AutoscrollStrategy {
|
||||
impl Editor {
|
||||
pub fn autoscroll_vertically(
|
||||
&mut self,
|
||||
viewport_height: Pixels,
|
||||
bounds: Bounds<Pixels>,
|
||||
line_height: Pixels,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let viewport_height = bounds.size.height;
|
||||
let visible_lines = viewport_height / line_height;
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
|
||||
let original_y = scroll_position.y;
|
||||
if let Some(last_bounds) = self.expect_bounds_change.take() {
|
||||
if scroll_position.y != 0. {
|
||||
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
|
||||
if scroll_position.y < 0. {
|
||||
scroll_position.y = 0.;
|
||||
}
|
||||
}
|
||||
}
|
||||
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
|
||||
} else {
|
||||
@@ -77,6 +87,9 @@ impl Editor {
|
||||
};
|
||||
if scroll_position.y > max_scroll_top {
|
||||
scroll_position.y = max_scroll_top;
|
||||
}
|
||||
|
||||
if original_y != scroll_position.y {
|
||||
self.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use gpui::AsyncAppContext;
|
||||
use language::{
|
||||
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::ops::Range;
|
||||
@@ -129,6 +129,23 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
None
|
||||
}
|
||||
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
let code_action_kinds = self
|
||||
.extension
|
||||
.manifest
|
||||
.language_servers
|
||||
.get(&self.language_server_id)
|
||||
.and_then(|server| server.code_action_kinds.clone());
|
||||
|
||||
code_action_kinds.or(Some(vec![
|
||||
CodeActionKind::EMPTY,
|
||||
CodeActionKind::QUICKFIX,
|
||||
CodeActionKind::REFACTOR,
|
||||
CodeActionKind::REFACTOR_EXTRACT,
|
||||
CodeActionKind::SOURCE,
|
||||
]))
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
// TODO: The language IDs can be provided via the language server options
|
||||
// in `extension.toml now but we're leaving these existing usages in place temporarily
|
||||
|
||||
@@ -106,6 +106,8 @@ pub struct LanguageServerManifestEntry {
|
||||
languages: Vec<Arc<str>>,
|
||||
#[serde(default)]
|
||||
pub language_ids: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
|
||||
}
|
||||
|
||||
impl LanguageServerManifestEntry {
|
||||
|
||||
@@ -4,15 +4,22 @@ use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ExtensionCard {
|
||||
overridden_by_dev_extension: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ExtensionCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
overridden_by_dev_extension: false,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self {
|
||||
self.overridden_by_dev_extension = overridden;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ExtensionCard {
|
||||
@@ -34,7 +41,24 @@ impl RenderOnce for ExtensionCard {
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.children(self.children),
|
||||
.children(self.children)
|
||||
.when(self.overridden_by_dev_extension, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.occlude()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(theme::color_alpha(
|
||||
cx.theme().colors().elevated_surface_background,
|
||||
0.8,
|
||||
))
|
||||
.child(Label::new("Overridden by dev extension.")),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("lua", &["lua"]),
|
||||
("make", &["Makefile"]),
|
||||
("nix", &["nix"]),
|
||||
("nu", &["nu"]),
|
||||
("ocaml", &["ml", "mli"]),
|
||||
("php", &["php"]),
|
||||
("prisma", &["prisma"]),
|
||||
@@ -59,7 +60,9 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("svelte", &["svelte"]),
|
||||
("swift", &["swift"]),
|
||||
("templ", &["templ"]),
|
||||
("terraform", &["tf", "tfvars", "hcl"]),
|
||||
("toml", &["Cargo.lock", "toml"]),
|
||||
("vue", &["vue"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("zig", &["zig"]),
|
||||
];
|
||||
|
||||
@@ -190,6 +190,15 @@ impl ExtensionsPage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether a dev extension currently exists for the extension with the given ID.
|
||||
fn dev_extension_exists(extension_id: &str, cx: &mut ViewContext<Self>) -> bool {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
extension_store
|
||||
.dev_extensions()
|
||||
.any(|dev_extension| dev_extension.id.as_ref() == extension_id)
|
||||
}
|
||||
|
||||
fn extension_status(extension_id: &str, cx: &mut ViewContext<Self>) -> ExtensionStatus {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
@@ -417,13 +426,21 @@ impl ExtensionsPage {
|
||||
) -> ExtensionCard {
|
||||
let this = cx.view().clone();
|
||||
let status = Self::extension_status(&extension.id, cx);
|
||||
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
|
||||
|
||||
let extension_id = extension.id.clone();
|
||||
let (install_or_uninstall_button, upgrade_button) =
|
||||
self.buttons_for_entry(extension, &status, cx);
|
||||
self.buttons_for_entry(extension, &status, has_dev_extension, cx);
|
||||
let version = extension.manifest.version.clone();
|
||||
let repository_url = extension.manifest.repository.clone();
|
||||
|
||||
let installed_version = match status {
|
||||
ExtensionStatus::Installed(installed_version) => Some(installed_version),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ExtensionCard::new()
|
||||
.overridden_by_dev_extension(has_dev_extension)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -435,9 +452,14 @@ impl ExtensionsPage {
|
||||
Headline::new(extension.manifest.name.clone())
|
||||
.size(HeadlineSize::Medium),
|
||||
)
|
||||
.child(
|
||||
Headline::new(format!("v{}", extension.manifest.version))
|
||||
.size(HeadlineSize::XSmall),
|
||||
.child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
|
||||
.children(
|
||||
installed_version
|
||||
.filter(|installed_version| *installed_version != version)
|
||||
.map(|installed_version| {
|
||||
Headline::new(format!("(v{installed_version} installed)",))
|
||||
.size(HeadlineSize::XSmall)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -577,16 +599,24 @@ impl ExtensionsPage {
|
||||
&self,
|
||||
extension: &ExtensionMetadata,
|
||||
status: &ExtensionStatus,
|
||||
has_dev_extension: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> (Button, Option<Button>) {
|
||||
let is_compatible = extension::is_version_compatible(&extension);
|
||||
let disabled = !is_compatible;
|
||||
|
||||
if has_dev_extension {
|
||||
// If we have a dev extension for the given extension, just treat it as uninstalled.
|
||||
// The button here is a placeholder, as it won't be interactable anyways.
|
||||
return (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
match status.clone() {
|
||||
ExtensionStatus::NotInstalled => (
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install")
|
||||
.disabled(disabled)
|
||||
.on_click(cx.listener({
|
||||
Button::new(SharedString::from(extension.id.clone()), "Install").on_click(
|
||||
cx.listener({
|
||||
let extension_id = extension.id.clone();
|
||||
move |this, _, cx| {
|
||||
this.telemetry
|
||||
@@ -595,7 +625,8 @@ impl ExtensionsPage {
|
||||
store.install_latest_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})),
|
||||
}),
|
||||
),
|
||||
None,
|
||||
),
|
||||
ExtensionStatus::Installing => (
|
||||
@@ -626,7 +657,20 @@ impl ExtensionsPage {
|
||||
} else {
|
||||
Some(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Upgrade")
|
||||
.disabled(disabled)
|
||||
.when(!is_compatible, |upgrade_button| {
|
||||
upgrade_button.disabled(true).tooltip({
|
||||
let version = extension.manifest.version.clone();
|
||||
move |cx| {
|
||||
Tooltip::text(
|
||||
format!(
|
||||
"v{version} is not compatible with this version of Zed.",
|
||||
),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.disabled(!is_compatible)
|
||||
.on_click(cx.listener({
|
||||
let extension_id = extension.id.clone();
|
||||
let version = extension.manifest.version.clone();
|
||||
|
||||
@@ -78,6 +78,7 @@ fn run_git_blame(
|
||||
.arg(path.as_os_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
util.workspace = true
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5"] }
|
||||
uuid.workspace = true
|
||||
waker-fn = "1.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1416,8 +1416,8 @@ pub struct AnyTooltip {
|
||||
/// The view used to display the tooltip
|
||||
pub view: AnyView,
|
||||
|
||||
/// The offset from the cursor to use, relative to the parent view
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
/// The absolute position of the mouse when the tooltip was deployed.
|
||||
pub mouse_position: Point<Pixels>,
|
||||
}
|
||||
|
||||
/// A keystroke event, and potentially the associated action
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::{channel::oneshot, Stream, StreamExt};
|
||||
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
|
||||
@@ -479,31 +479,26 @@ impl TestAppContext {
|
||||
|
||||
impl<T: 'static> Model<T> {
|
||||
/// Block until the next event is emitted by the model, then return it.
|
||||
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
|
||||
pub fn next_event<Event>(&self, cx: &mut TestAppContext) -> impl Future<Output = Event>
|
||||
where
|
||||
Evt: Send + Clone + 'static,
|
||||
T: EventEmitter<Evt>,
|
||||
Event: Send + Clone + 'static,
|
||||
T: EventEmitter<Event>,
|
||||
{
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
let _subscription = self.update(cx, |_, cx| {
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
let mut tx = Some(tx);
|
||||
let subscription = self.update(cx, |_, cx| {
|
||||
cx.subscribe(self, move |_, _, event, _| {
|
||||
tx.unbounded_send(event.clone()).ok();
|
||||
if let Some(tx) = tx.take() {
|
||||
_ = tx.send(event.clone());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Run other tasks until the event is emitted.
|
||||
loop {
|
||||
match rx.try_next() {
|
||||
Ok(Some(event)) => return event,
|
||||
Ok(None) => panic!("model was dropped"),
|
||||
Err(_) => {
|
||||
if !cx.executor().tick() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async move {
|
||||
let event = rx.await.expect("no event emitted");
|
||||
drop(subscription);
|
||||
event
|
||||
}
|
||||
panic!("no event received")
|
||||
}
|
||||
|
||||
/// Returns a future that resolves when the model notifies.
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
|
||||
StyleRefinement, Styled, Task, View, Visibility, WindowContext,
|
||||
StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use refineable::Refineable;
|
||||
@@ -483,7 +483,29 @@ impl Interactivity {
|
||||
self.tooltip_builder.is_none(),
|
||||
"calling tooltip more than once on the same element is not supported"
|
||||
);
|
||||
self.tooltip_builder = Some(Rc::new(build_tooltip));
|
||||
self.tooltip_builder = Some(TooltipBuilder {
|
||||
build: Rc::new(build_tooltip),
|
||||
hoverable: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`]
|
||||
pub fn hoverable_tooltip(
|
||||
&mut self,
|
||||
build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
debug_assert!(
|
||||
self.tooltip_builder.is_none(),
|
||||
"calling tooltip more than once on the same element is not supported"
|
||||
);
|
||||
self.tooltip_builder = Some(TooltipBuilder {
|
||||
build: Rc::new(build_tooltip),
|
||||
hoverable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Block the mouse from interacting with this element or any of its children
|
||||
@@ -973,6 +995,20 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
self.interactivity().tooltip(build_tooltip);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
|
||||
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
|
||||
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]
|
||||
fn hoverable_tooltip(
|
||||
mut self,
|
||||
build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().hoverable_tooltip(build_tooltip);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for providing focus related APIs to interactive elements
|
||||
@@ -1015,7 +1051,10 @@ type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
|
||||
|
||||
type CanDropPredicate = Box<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>;
|
||||
|
||||
pub(crate) type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
|
||||
pub(crate) struct TooltipBuilder {
|
||||
build: Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
|
||||
hoverable: bool,
|
||||
}
|
||||
|
||||
pub(crate) type KeyDownListener =
|
||||
Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>;
|
||||
@@ -1188,6 +1227,7 @@ pub struct Interactivity {
|
||||
/// Whether the element was hovered. This will only be present after paint if an hitbox
|
||||
/// was created for the interactive element.
|
||||
pub hovered: Option<bool>,
|
||||
pub(crate) tooltip_id: Option<TooltipId>,
|
||||
pub(crate) content_size: Size<Pixels>,
|
||||
pub(crate) key_context: Option<KeyContext>,
|
||||
pub(crate) focusable: bool,
|
||||
@@ -1321,7 +1361,7 @@ impl Interactivity {
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
|
||||
if let Some(active_tooltip) = active_tooltip.borrow().as_ref() {
|
||||
if let Some(tooltip) = active_tooltip.tooltip.clone() {
|
||||
cx.set_tooltip(tooltip);
|
||||
self.tooltip_id = Some(cx.set_tooltip(tooltip));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1806,6 +1846,7 @@ impl Interactivity {
|
||||
}
|
||||
|
||||
if let Some(tooltip_builder) = self.tooltip_builder.take() {
|
||||
let tooltip_is_hoverable = tooltip_builder.hoverable;
|
||||
let active_tooltip = element_state
|
||||
.active_tooltip
|
||||
.get_or_insert_with(Default::default)
|
||||
@@ -1818,11 +1859,17 @@ impl Interactivity {
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &MouseMoveEvent, phase, cx| {
|
||||
let is_hovered =
|
||||
pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx);
|
||||
if !is_hovered {
|
||||
active_tooltip.borrow_mut().take();
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
if !is_hovered && (!tooltip_is_hoverable || !tooltip_is_hovered) {
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.refresh();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1833,15 +1880,14 @@ impl Interactivity {
|
||||
if active_tooltip.borrow().is_none() {
|
||||
let task = cx.spawn({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
let tooltip_builder = tooltip_builder.clone();
|
||||
|
||||
let build_tooltip = tooltip_builder.build.clone();
|
||||
move |mut cx| async move {
|
||||
cx.background_executor().timer(TOOLTIP_DELAY).await;
|
||||
cx.update(|cx| {
|
||||
active_tooltip.borrow_mut().replace(ActiveTooltip {
|
||||
tooltip: Some(AnyTooltip {
|
||||
view: tooltip_builder(cx),
|
||||
cursor_offset: cx.mouse_position(),
|
||||
view: build_tooltip(cx),
|
||||
mouse_position: cx.mouse_position(),
|
||||
}),
|
||||
_task: None,
|
||||
});
|
||||
@@ -1860,15 +1906,30 @@ impl Interactivity {
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |_: &MouseDownEvent, _, _| {
|
||||
active_tooltip.borrow_mut().take();
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &MouseDownEvent, _, cx| {
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
|
||||
if !tooltip_is_hoverable || !tooltip_is_hovered {
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cx.on_mouse_event({
|
||||
let active_tooltip = active_tooltip.clone();
|
||||
move |_: &ScrollWheelEvent, _, _| {
|
||||
active_tooltip.borrow_mut().take();
|
||||
let tooltip_id = self.tooltip_id;
|
||||
move |_: &ScrollWheelEvent, _, cx| {
|
||||
let tooltip_is_hovered =
|
||||
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
|
||||
if !tooltip_is_hoverable || !tooltip_is_hovered {
|
||||
if active_tooltip.borrow_mut().take().is_some() {
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ impl Element for InteractiveText {
|
||||
ActiveTooltip {
|
||||
tooltip: Some(AnyTooltip {
|
||||
view: tooltip,
|
||||
cursor_offset: cx.mouse_position(),
|
||||
mouse_position: cx.mouse_position(),
|
||||
}),
|
||||
_task: None,
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ impl BackgroundExecutor {
|
||||
self.dispatcher.as_test().unwrap().rng()
|
||||
}
|
||||
|
||||
/// How many CPUs are available to the dispatcher
|
||||
/// How many CPUs are available to the dispatcher.
|
||||
pub fn num_cpus(&self) -> usize {
|
||||
num_cpus::get()
|
||||
}
|
||||
@@ -440,6 +440,11 @@ impl<'a> Scope<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many CPUs are available to the dispatcher.
|
||||
pub fn num_cpus(&self) -> usize {
|
||||
self.executor.num_cpus()
|
||||
}
|
||||
|
||||
/// Spawn a future into this scope.
|
||||
pub fn spawn<F>(&mut self, f: F)
|
||||
where
|
||||
|
||||
@@ -1257,6 +1257,7 @@ where
|
||||
/// origin: Point { x: 15.0, y: 15.0 },
|
||||
/// size: Size { width: 15.0, height: 30.0 },
|
||||
/// });
|
||||
/// ```
|
||||
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
|
||||
where
|
||||
U: Clone + Default + Debug,
|
||||
@@ -1283,6 +1284,7 @@ where
|
||||
/// origin: Point { x: 15.0, y: 15.0 },
|
||||
/// size: Size { width: 10.0, height: 20.0 },
|
||||
/// });
|
||||
/// ```
|
||||
pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> {
|
||||
Bounds {
|
||||
origin: f(self.origin),
|
||||
|
||||
@@ -162,7 +162,7 @@ impl BladeAtlasState {
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
format = gpu::TextureFormat::Rgba8Unorm;
|
||||
format = gpu::TextureFormat::Bgra8Unorm;
|
||||
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
|
||||
}
|
||||
AtlasTextureKind::Path => {
|
||||
|
||||
@@ -95,6 +95,7 @@ impl Globals {
|
||||
|
||||
pub(crate) struct WaylandClientState {
|
||||
globals: Globals,
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
// Surface to Window mapping
|
||||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
// Output to scale mapping
|
||||
@@ -103,9 +104,13 @@ pub(crate) struct WaylandClientState {
|
||||
click: ClickState,
|
||||
repeat: KeyRepeat,
|
||||
modifiers: Modifiers,
|
||||
scroll_direction: f64,
|
||||
axis_source: AxisSource,
|
||||
mouse_location: Option<Point<Pixels>>,
|
||||
continuous_scroll_delta: Option<Point<Pixels>>,
|
||||
discrete_scroll_delta: Option<Point<f32>>,
|
||||
vertical_modifier: f32,
|
||||
horizontal_modifier: f32,
|
||||
scroll_event_received: bool,
|
||||
enter_token: Option<()>,
|
||||
button_pressed: Option<MouseButton>,
|
||||
mouse_focused_window: Option<WaylandWindowStatePtr>,
|
||||
@@ -164,20 +169,21 @@ impl WaylandClientStatePtr {
|
||||
#[derive(Clone)]
|
||||
pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
|
||||
|
||||
const WL_SEAT_MIN_VERSION: u32 = 4;
|
||||
const WL_OUTPUT_VERSION: u32 = 2;
|
||||
|
||||
fn wl_seat_version(version: u32) -> u32 {
|
||||
if version >= wl_pointer::EVT_AXIS_VALUE120_SINCE {
|
||||
wl_pointer::EVT_AXIS_VALUE120_SINCE
|
||||
} else if version >= WL_SEAT_MIN_VERSION {
|
||||
WL_SEAT_MIN_VERSION
|
||||
} else {
|
||||
// We rely on the wl_pointer.frame event
|
||||
const WL_SEAT_MIN_VERSION: u32 = 5;
|
||||
const WL_SEAT_MAX_VERSION: u32 = 9;
|
||||
|
||||
if version < WL_SEAT_MIN_VERSION {
|
||||
panic!(
|
||||
"wl_seat below required version: {} < {}",
|
||||
version, WL_SEAT_MIN_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
@@ -235,6 +241,7 @@ impl WaylandClient {
|
||||
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
globals,
|
||||
wl_pointer: None,
|
||||
output_scales: outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
@@ -257,9 +264,13 @@ impl WaylandClient {
|
||||
function: false,
|
||||
platform: false,
|
||||
},
|
||||
scroll_direction: -1.0,
|
||||
scroll_event_received: false,
|
||||
axis_source: AxisSource::Wheel,
|
||||
mouse_location: None,
|
||||
continuous_scroll_delta: None,
|
||||
discrete_scroll_delta: None,
|
||||
vertical_modifier: -1.0,
|
||||
horizontal_modifier: -1.0,
|
||||
button_pressed: None,
|
||||
mouse_focused_window: None,
|
||||
keyboard_focused_window: None,
|
||||
@@ -334,7 +345,15 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
.to_string();
|
||||
|
||||
self.0.borrow_mut().cursor_icon_name = cursor_icon_name;
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.cursor_icon_name = cursor_icon_name.clone();
|
||||
if state.mouse_focused_window.is_some() {
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
state.cursor.set_icon(&wl_pointer, &cursor_icon_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
@@ -394,6 +413,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
version,
|
||||
} => match &interface[..] {
|
||||
"wl_seat" => {
|
||||
state.wl_pointer = None;
|
||||
registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
|
||||
}
|
||||
"wl_output" => {
|
||||
@@ -573,7 +593,9 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
seat.get_keyboard(qh, ());
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
seat.get_pointer(qh, ());
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
state.wl_pointer = Some(seat.get_pointer(qh, ()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -780,10 +802,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
state.enter_token = Some(());
|
||||
state.mouse_focused_window = Some(window.clone());
|
||||
state.cursor.mark_dirty();
|
||||
state.cursor.set_serial_id(serial);
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
|
||||
.set_icon(&wl_pointer, cursor_icon_name.as_str());
|
||||
drop(state);
|
||||
window.set_focused(true);
|
||||
}
|
||||
@@ -814,9 +837,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
return;
|
||||
}
|
||||
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
|
||||
state
|
||||
.cursor
|
||||
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
|
||||
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
let input = PlatformInput::MouseMove(MouseMoveEvent {
|
||||
@@ -887,77 +907,137 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::AxisRelativeDirection {
|
||||
direction: WEnum::Value(direction),
|
||||
..
|
||||
} => {
|
||||
state.scroll_direction = match direction {
|
||||
AxisRelativeDirection::Identical => -1.0,
|
||||
AxisRelativeDirection::Inverted => 1.0,
|
||||
_ => -1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Axis Events
|
||||
wl_pointer::Event::AxisSource {
|
||||
axis_source: WEnum::Value(axis_source),
|
||||
} => {
|
||||
state.axis_source = axis_source;
|
||||
}
|
||||
wl_pointer::Event::AxisValue120 {
|
||||
axis: WEnum::Value(axis),
|
||||
value120,
|
||||
} => {
|
||||
if let Some(focused_window) = state.mouse_focused_window.clone() {
|
||||
let value = value120 as f64 * state.scroll_direction;
|
||||
|
||||
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
delta: match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
modifiers: state.modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
drop(state);
|
||||
focused_window.handle_input(input)
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Axis {
|
||||
time,
|
||||
axis: WEnum::Value(axis),
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
// We handle discrete scroll events with `AxisValue120`.
|
||||
if wl_pointer.version() >= wl_pointer::EVT_AXIS_VALUE120_SINCE
|
||||
&& state.axis_source == AxisSource::Wheel
|
||||
{
|
||||
return;
|
||||
let axis_source = state.axis_source;
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
_ => 1.0,
|
||||
};
|
||||
let supports_relative_direction =
|
||||
wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
|
||||
state.scroll_event_received = true;
|
||||
let scroll_delta = state
|
||||
.continuous_scroll_delta
|
||||
.get_or_insert(point(px(0.0), px(0.0)));
|
||||
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
|
||||
let modifier = 3.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += px(value as f32 * modifier * axis_modifier);
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += px(value as f32 * modifier * axis_modifier);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if let Some(focused_window) = state.mouse_focused_window.clone() {
|
||||
let value = value * state.scroll_direction;
|
||||
}
|
||||
wl_pointer::Event::AxisDiscrete {
|
||||
axis: WEnum::Value(axis),
|
||||
discrete,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
delta: match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
modifiers: state.modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
drop(state);
|
||||
focused_window.handle_input(input)
|
||||
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
|
||||
let modifier = 3.0;
|
||||
|
||||
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += discrete as f32 * axis_modifier * modifier;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += discrete as f32 * axis_modifier * modifier;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::AxisRelativeDirection {
|
||||
axis: WEnum::Value(axis),
|
||||
direction: WEnum::Value(direction),
|
||||
} => match (axis, direction) {
|
||||
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Identical) => {
|
||||
state.vertical_modifier = -1.0
|
||||
}
|
||||
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Inverted) => {
|
||||
state.vertical_modifier = 1.0
|
||||
}
|
||||
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Identical) => {
|
||||
state.horizontal_modifier = -1.0
|
||||
}
|
||||
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Inverted) => {
|
||||
state.horizontal_modifier = 1.0
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
wl_pointer::Event::AxisValue120 {
|
||||
axis: WEnum::Value(axis),
|
||||
value120,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
|
||||
let wheel_percent = value120 as f32 / 120.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += wheel_percent * axis_modifier;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += wheel_percent * axis_modifier;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Frame => {
|
||||
if state.scroll_event_received {
|
||||
state.scroll_event_received = false;
|
||||
let continuous = state.continuous_scroll_delta.take();
|
||||
let discrete = state.discrete_scroll_delta.take();
|
||||
if let Some(continuous) = continuous {
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
delta: ScrollDelta::Pixels(continuous),
|
||||
modifiers: state.modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
}
|
||||
} else if let Some(discrete) = discrete {
|
||||
if let Some(window) = state.mouse_focused_window.clone() {
|
||||
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
delta: ScrollDelta::Lines(discrete),
|
||||
modifiers: state.modifiers,
|
||||
touch_phase: TouchPhase::Moved,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -8,7 +8,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme};
|
||||
|
||||
pub(crate) struct Cursor {
|
||||
theme: Option<CursorTheme>,
|
||||
current_icon_name: String,
|
||||
current_icon_name: Option<String>,
|
||||
surface: WlSurface,
|
||||
serial_id: u32,
|
||||
}
|
||||
@@ -24,19 +24,29 @@ impl Cursor {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||
Self {
|
||||
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
|
||||
current_icon_name: "default".to_string(),
|
||||
current_icon_name: None,
|
||||
surface: globals.compositor.create_surface(&globals.qh, ()),
|
||||
serial_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.current_icon_name = None;
|
||||
}
|
||||
|
||||
pub fn set_serial_id(&mut self, serial_id: u32) {
|
||||
self.serial_id = serial_id;
|
||||
}
|
||||
|
||||
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: Option<&str>) {
|
||||
let mut cursor_icon_name = cursor_icon_name.unwrap_or("default");
|
||||
if self.current_icon_name != cursor_icon_name {
|
||||
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: &str) {
|
||||
let need_update = self
|
||||
.current_icon_name
|
||||
.as_ref()
|
||||
.map_or(true, |current_icon_name| {
|
||||
current_icon_name != cursor_icon_name
|
||||
});
|
||||
|
||||
if need_update {
|
||||
if let Some(theme) = &mut self.theme {
|
||||
let mut buffer: Option<&CursorImageBuffer>;
|
||||
|
||||
@@ -68,7 +78,7 @@ impl Cursor {
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
|
||||
self.current_icon_name = cursor_icon_name.to_string();
|
||||
self.current_icon_name = Some(cursor_icon_name.to_string());
|
||||
}
|
||||
} else {
|
||||
log::warn!("Linux: Wayland: Unable to load cursor themes");
|
||||
|
||||
@@ -33,8 +33,10 @@ use super::X11Display;
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
UTF8_STRING,
|
||||
WM_PROTOCOLS,
|
||||
WM_DELETE_WINDOW,
|
||||
_NET_WM_NAME,
|
||||
_NET_WM_STATE,
|
||||
_NET_WM_STATE_MAXIMIZED_VERT,
|
||||
_NET_WM_STATE_MAXIMIZED_HORZ,
|
||||
@@ -76,7 +78,7 @@ pub struct Callbacks {
|
||||
|
||||
pub(crate) struct X11WindowState {
|
||||
raw: RawWindow,
|
||||
|
||||
atoms: XcbAtoms,
|
||||
bounds: Bounds<i32>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
@@ -238,6 +240,7 @@ impl X11WindowState {
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor: 1.0,
|
||||
renderer: BladeRenderer::new(gpu, gpu_extent),
|
||||
atoms: *atoms,
|
||||
|
||||
input_handler: None,
|
||||
}
|
||||
@@ -442,6 +445,16 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.x_window,
|
||||
self.state.borrow().atoms._NET_WM_NAME,
|
||||
self.state.borrow().atoms.UTF8_STRING,
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
|
||||
@@ -570,6 +570,7 @@ impl Platform for MacPlatform {
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block];
|
||||
}
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ impl MacTextSystemState {
|
||||
self.postscript_names_by_font_id
|
||||
.get(&font_id)
|
||||
.map_or(false, |postscript_name| {
|
||||
postscript_name == "AppleColorEmoji"
|
||||
postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -121,21 +121,18 @@ impl WindowsDisplay {
|
||||
}
|
||||
|
||||
pub(crate) fn frequency(&self) -> Option<u32> {
|
||||
available_monitors()
|
||||
.get(self.display_id.0 as usize)
|
||||
.and_then(|hmonitor| get_monitor_info(*hmonitor).ok())
|
||||
.and_then(|info| {
|
||||
let mut devmode = DEVMODEW::default();
|
||||
unsafe {
|
||||
EnumDisplaySettingsW(
|
||||
PCWSTR(info.szDevice.as_ptr()),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mut devmode,
|
||||
)
|
||||
}
|
||||
.as_bool()
|
||||
.then(|| devmode.dmDisplayFrequency)
|
||||
})
|
||||
get_monitor_info(self.handle).ok().and_then(|info| {
|
||||
let mut devmode = DEVMODEW::default();
|
||||
unsafe {
|
||||
EnumDisplaySettingsW(
|
||||
PCWSTR(info.szDevice.as_ptr()),
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
&mut devmode,
|
||||
)
|
||||
}
|
||||
.as_bool()
|
||||
.then(|| devmode.dmDisplayFrequency)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -687,8 +687,10 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
ctx.set_contents(item.text().to_owned()).unwrap();
|
||||
if item.text.len() > 0 {
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
ctx.set_contents(item.text().to_owned()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
|
||||
@@ -52,7 +52,6 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
display: RefCell<Rc<WindowsDisplay>>,
|
||||
last_ime_input: RefCell<Option<String>>,
|
||||
click_state: RefCell<ClickState>,
|
||||
fullscreen: Cell<Option<StyleAndBounds>>,
|
||||
}
|
||||
@@ -114,7 +113,6 @@ impl WindowsWindowInner {
|
||||
let renderer = RefCell::new(BladeRenderer::new(gpu, extent));
|
||||
let callbacks = RefCell::new(Callbacks::default());
|
||||
let display = RefCell::new(display);
|
||||
let last_ime_input = RefCell::new(None);
|
||||
let click_state = RefCell::new(ClickState::new());
|
||||
let fullscreen = Cell::new(None);
|
||||
Self {
|
||||
@@ -129,7 +127,6 @@ impl WindowsWindowInner {
|
||||
handle,
|
||||
hide_title_bar,
|
||||
display,
|
||||
last_ime_input,
|
||||
click_state,
|
||||
fullscreen,
|
||||
}
|
||||
@@ -816,6 +813,7 @@ impl WindowsWindowInner {
|
||||
}
|
||||
|
||||
fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
|
||||
let mut ime_input = None;
|
||||
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
|
||||
let Some((string, string_len)) = self.parse_ime_compostion_string() else {
|
||||
return None;
|
||||
@@ -829,10 +827,10 @@ impl WindowsWindowInner {
|
||||
Some(0..string_len),
|
||||
);
|
||||
self.input_handler.set(Some(input_handler));
|
||||
*self.last_ime_input.borrow_mut() = Some(string);
|
||||
ime_input = Some(string);
|
||||
}
|
||||
if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
|
||||
let Some(ref comp_string) = *self.last_ime_input.borrow() else {
|
||||
let Some(ref comp_string) = ime_input else {
|
||||
return None;
|
||||
};
|
||||
let caret_pos = self.retrieve_composition_cursor_position();
|
||||
@@ -863,7 +861,6 @@ impl WindowsWindowInner {
|
||||
};
|
||||
input_handler.replace_text_in_range(None, &ime_char);
|
||||
self.input_handler.set(Some(input_handler));
|
||||
*self.last_ime_input.borrow_mut() = None;
|
||||
self.invalidate_client_area();
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -287,6 +287,8 @@ pub struct Window {
|
||||
pub(crate) rendered_frame: Frame,
|
||||
pub(crate) next_frame: Frame,
|
||||
pub(crate) next_hitbox_id: HitboxId,
|
||||
pub(crate) next_tooltip_id: TooltipId,
|
||||
pub(crate) tooltip_bounds: Option<TooltipBounds>,
|
||||
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
|
||||
pub(crate) dirty_views: FxHashSet<EntityId>,
|
||||
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
|
||||
@@ -551,6 +553,8 @@ impl Window {
|
||||
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
|
||||
next_frame_callbacks,
|
||||
next_hitbox_id: HitboxId::default(),
|
||||
next_tooltip_id: TooltipId::default(),
|
||||
tooltip_bounds: None,
|
||||
dirty_views: FxHashSet::default(),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
focus_listeners: SubscriberSet::new(),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut, Cow},
|
||||
mem,
|
||||
cmp, mem,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
@@ -28,17 +28,18 @@ use futures::{future::Shared, FutureExt};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
use smallvec::SmallVec;
|
||||
use util::post_inc;
|
||||
|
||||
use crate::{
|
||||
hash, prelude::*, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, Bounds,
|
||||
BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase,
|
||||
DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId,
|
||||
GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent,
|
||||
LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad,
|
||||
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle,
|
||||
Style, Task, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window,
|
||||
WindowContext, SUBPIXEL_VARIANTS,
|
||||
hash, point, prelude::*, px, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace,
|
||||
Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId,
|
||||
DispatchPhase, DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle,
|
||||
FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext,
|
||||
KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent,
|
||||
PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
|
||||
RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
|
||||
StrikethroughStyle, Style, Task, TextStyleRefinement, TransformationMatrix, Underline,
|
||||
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
|
||||
pub(crate) type AnyMouseListener =
|
||||
@@ -65,11 +66,13 @@ impl HitboxId {
|
||||
/// See [ElementContext::insert_hitbox] for more details.
|
||||
#[derive(Clone, Debug, Deref)]
|
||||
pub struct Hitbox {
|
||||
/// A unique identifier for the hitbox
|
||||
/// A unique identifier for the hitbox.
|
||||
pub id: HitboxId,
|
||||
/// The bounds of the hitbox
|
||||
/// The bounds of the hitbox.
|
||||
#[deref]
|
||||
pub bounds: Bounds<Pixels>,
|
||||
/// The content mask when the hitbox was inserted.
|
||||
pub content_mask: ContentMask<Pixels>,
|
||||
/// Whether the hitbox occludes other hitboxes inserted prior.
|
||||
pub opaque: bool,
|
||||
}
|
||||
@@ -84,6 +87,33 @@ impl Hitbox {
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>);
|
||||
|
||||
/// An identifier for a tooltip.
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TooltipId(usize);
|
||||
|
||||
impl TooltipId {
|
||||
/// Checks if the tooltip is currently hovered.
|
||||
pub fn is_hovered(&self, cx: &WindowContext) -> bool {
|
||||
cx.window
|
||||
.tooltip_bounds
|
||||
.as_ref()
|
||||
.map_or(false, |tooltip_bounds| {
|
||||
tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TooltipBounds {
|
||||
id: TooltipId,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TooltipRequest {
|
||||
id: TooltipId,
|
||||
tooltip: AnyTooltip,
|
||||
}
|
||||
|
||||
pub(crate) struct DeferredDraw {
|
||||
priority: usize,
|
||||
parent_node: DispatchNodeId,
|
||||
@@ -108,7 +138,7 @@ pub(crate) struct Frame {
|
||||
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
|
||||
pub(crate) input_handlers: Vec<Option<PlatformInputHandler>>,
|
||||
pub(crate) tooltip_requests: Vec<Option<AnyTooltip>>,
|
||||
pub(crate) tooltip_requests: Vec<Option<TooltipRequest>>,
|
||||
pub(crate) cursor_styles: Vec<CursorStyleRequest>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
|
||||
@@ -173,7 +203,8 @@ impl Frame {
|
||||
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
|
||||
let mut hit_test = HitTest::default();
|
||||
for hitbox in self.hitboxes.iter().rev() {
|
||||
if hitbox.bounds.contains(&position) {
|
||||
let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds);
|
||||
if bounds.contains(&position) {
|
||||
hit_test.0.push(hitbox.id);
|
||||
if hitbox.opaque {
|
||||
break;
|
||||
@@ -364,6 +395,7 @@ impl<'a> VisualContext for ElementContext<'a> {
|
||||
impl<'a> ElementContext<'a> {
|
||||
pub(crate) fn draw_roots(&mut self) {
|
||||
self.window.draw_phase = DrawPhase::Layout;
|
||||
self.window.tooltip_bounds.take();
|
||||
|
||||
// Layout all root elements.
|
||||
let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any();
|
||||
@@ -388,14 +420,8 @@ impl<'a> ElementContext<'a> {
|
||||
element.layout(offset, AvailableSpace::min_size(), self);
|
||||
active_drag_element = Some(element);
|
||||
self.app.active_drag = Some(active_drag);
|
||||
} else if let Some(tooltip_request) =
|
||||
self.window.next_frame.tooltip_requests.last().cloned()
|
||||
{
|
||||
let tooltip_request = tooltip_request.unwrap();
|
||||
let mut element = tooltip_request.view.clone().into_any();
|
||||
let offset = tooltip_request.cursor_offset;
|
||||
element.layout(offset, AvailableSpace::min_size(), self);
|
||||
tooltip_element = Some(element);
|
||||
} else {
|
||||
tooltip_element = self.layout_tooltip();
|
||||
}
|
||||
|
||||
self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position);
|
||||
@@ -415,6 +441,52 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_tooltip(&mut self) -> Option<AnyElement> {
|
||||
let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?;
|
||||
let tooltip_request = tooltip_request.unwrap();
|
||||
let mut element = tooltip_request.tooltip.view.clone().into_any();
|
||||
let mouse_position = tooltip_request.tooltip.mouse_position;
|
||||
let tooltip_size = element.measure(AvailableSpace::min_size(), self);
|
||||
|
||||
let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
|
||||
let window_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: self.viewport_size(),
|
||||
};
|
||||
|
||||
if tooltip_bounds.right() > window_bounds.right() {
|
||||
let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.);
|
||||
if new_x >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.x = new_x;
|
||||
} else {
|
||||
tooltip_bounds.origin.x = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if tooltip_bounds.bottom() > window_bounds.bottom() {
|
||||
let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.);
|
||||
if new_y >= Pixels::ZERO {
|
||||
tooltip_bounds.origin.y = new_y;
|
||||
} else {
|
||||
tooltip_bounds.origin.y = cmp::max(
|
||||
Pixels::ZERO,
|
||||
tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx));
|
||||
|
||||
self.window.tooltip_bounds = Some(TooltipBounds {
|
||||
id: tooltip_request.id,
|
||||
bounds: tooltip_bounds,
|
||||
});
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn layout_deferred_draws(&mut self, deferred_draw_indices: &[usize]) {
|
||||
assert_eq!(self.window.element_id_stack.len(), 0);
|
||||
|
||||
@@ -604,8 +676,13 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
|
||||
/// Sets a tooltip to be rendered for the upcoming frame
|
||||
pub fn set_tooltip(&mut self, tooltip: AnyTooltip) {
|
||||
self.window.next_frame.tooltip_requests.push(Some(tooltip));
|
||||
pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId {
|
||||
let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0));
|
||||
self.window
|
||||
.next_frame
|
||||
.tooltip_requests
|
||||
.push(Some(TooltipRequest { id, tooltip }));
|
||||
id
|
||||
}
|
||||
|
||||
/// Pushes the given element id onto the global stack and invokes the given closure
|
||||
@@ -1330,7 +1407,8 @@ impl<'a> ElementContext<'a> {
|
||||
window.next_hitbox_id.0 += 1;
|
||||
let hitbox = Hitbox {
|
||||
id,
|
||||
bounds: bounds.intersect(&content_mask.bounds),
|
||||
bounds,
|
||||
content_mask,
|
||||
opaque,
|
||||
};
|
||||
window.next_frame.hitboxes.push(hitbox.clone());
|
||||
|
||||
@@ -45,9 +45,9 @@ use text::operation_queue::OperationQueue;
|
||||
use text::*;
|
||||
pub use text::{
|
||||
Anchor, Bias, Buffer as TextBuffer, BufferId, BufferSnapshot as TextBufferSnapshot, Edit,
|
||||
OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, RopeFingerprint, Selection,
|
||||
SelectionGoal, Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint,
|
||||
ToPointUtf16, Transaction, TransactionId, Unclipped,
|
||||
OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, Selection, SelectionGoal,
|
||||
Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16,
|
||||
Transaction, TransactionId, Unclipped,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -87,8 +87,6 @@ pub struct Buffer {
|
||||
/// The version vector when this buffer was last loaded from
|
||||
/// or saved to disk.
|
||||
saved_version: clock::Global,
|
||||
/// A hash of the current contents of the buffer's file.
|
||||
file_fingerprint: RopeFingerprint,
|
||||
transaction_depth: usize,
|
||||
was_dirty_before_starting_transaction: Option<bool>,
|
||||
reload_task: Option<Task<Result<()>>>,
|
||||
@@ -379,7 +377,6 @@ pub trait LocalFile: File {
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
version: &clock::Global,
|
||||
fingerprint: RopeFingerprint,
|
||||
line_ending: LineEnding,
|
||||
mtime: Option<SystemTime>,
|
||||
cx: &mut AppContext,
|
||||
@@ -562,7 +559,6 @@ impl Buffer {
|
||||
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||
saved_version: proto::serialize_version(&self.saved_version),
|
||||
saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
|
||||
saved_mtime: self.saved_mtime.map(|time| time.into()),
|
||||
}
|
||||
}
|
||||
@@ -642,7 +638,6 @@ impl Buffer {
|
||||
Self {
|
||||
saved_mtime,
|
||||
saved_version: buffer.version(),
|
||||
file_fingerprint: buffer.as_rope().fingerprint(),
|
||||
reload_task: None,
|
||||
transaction_depth: 0,
|
||||
was_dirty_before_starting_transaction: None,
|
||||
@@ -717,11 +712,6 @@ impl Buffer {
|
||||
&self.saved_version
|
||||
}
|
||||
|
||||
/// The fingerprint of the buffer's text when the buffer was last saved or reloaded from disk.
|
||||
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
|
||||
self.file_fingerprint
|
||||
}
|
||||
|
||||
/// The mtime of the buffer's file when the buffer was last saved or reloaded from disk.
|
||||
pub fn saved_mtime(&self) -> Option<SystemTime> {
|
||||
self.saved_mtime
|
||||
@@ -754,13 +744,11 @@ impl Buffer {
|
||||
pub fn did_save(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
fingerprint: RopeFingerprint,
|
||||
mtime: Option<SystemTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
self.has_conflict = false;
|
||||
self.file_fingerprint = fingerprint;
|
||||
self.saved_mtime = mtime;
|
||||
cx.emit(Event::Saved);
|
||||
cx.notify();
|
||||
@@ -792,13 +780,7 @@ impl Buffer {
|
||||
this.apply_diff(diff, cx);
|
||||
tx.send(this.finalize_last_transaction().cloned()).ok();
|
||||
this.has_conflict = false;
|
||||
this.did_reload(
|
||||
this.version(),
|
||||
this.as_rope().fingerprint(),
|
||||
this.line_ending(),
|
||||
new_mtime,
|
||||
cx,
|
||||
);
|
||||
this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
|
||||
} else {
|
||||
if !diff.edits.is_empty()
|
||||
|| this
|
||||
@@ -809,13 +791,7 @@ impl Buffer {
|
||||
this.has_conflict = true;
|
||||
}
|
||||
|
||||
this.did_reload(
|
||||
prev_version,
|
||||
Rope::text_fingerprint(&new_text),
|
||||
this.line_ending(),
|
||||
this.saved_mtime,
|
||||
cx,
|
||||
);
|
||||
this.did_reload(prev_version, this.line_ending(), this.saved_mtime, cx);
|
||||
}
|
||||
|
||||
this.reload_task.take();
|
||||
@@ -828,20 +804,17 @@ impl Buffer {
|
||||
pub fn did_reload(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
fingerprint: RopeFingerprint,
|
||||
line_ending: LineEnding,
|
||||
mtime: Option<SystemTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.saved_version = version;
|
||||
self.file_fingerprint = fingerprint;
|
||||
self.text.set_line_ending(line_ending);
|
||||
self.saved_mtime = mtime;
|
||||
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
|
||||
file.buffer_reloaded(
|
||||
self.remote_id(),
|
||||
&self.saved_version,
|
||||
self.file_fingerprint,
|
||||
self.line_ending(),
|
||||
self.saved_mtime,
|
||||
cx,
|
||||
|
||||
@@ -56,7 +56,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use syntax_map::SyntaxSnapshot;
|
||||
pub use task_context::{ContextProvider, ContextProviderWithTasks, SymbolContextProvider};
|
||||
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, wasmtime, Query, WasmStore};
|
||||
use util::http::HttpClient;
|
||||
@@ -72,7 +72,7 @@ pub use lsp::LanguageServerId;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
|
||||
pub use text::LineEnding;
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
||||
use crate::language_settings::SoftWrap;
|
||||
|
||||
@@ -91,6 +91,16 @@ thread_local! {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn with_parser<F, R>(func: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Parser) -> R,
|
||||
{
|
||||
PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
func(&mut parser)
|
||||
})
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default();
|
||||
static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();
|
||||
|
||||
@@ -5,10 +5,10 @@ use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use crate::{HighlightId, Language, LanguageRegistry};
|
||||
use gpui::{px, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, UnderlineStyle};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
|
||||
|
||||
/// Parsed Markdown content.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParsedMarkdown {
|
||||
/// The Markdown text.
|
||||
pub text: String,
|
||||
@@ -165,7 +165,10 @@ pub async fn parse_markdown_block(
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(markdown, Options::all()) {
|
||||
let mut options = pulldown_cmark::Options::all();
|
||||
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
|
||||
|
||||
for event in Parser::new_ext(markdown, options) {
|
||||
let prev_len = text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
@@ -249,7 +252,7 @@ pub async fn parse_markdown_block(
|
||||
new_paragraph(text, &mut list_stack);
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.language_for_name_or_extension(language.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
@@ -357,3 +360,35 @@ pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)
|
||||
text.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dividers() {
|
||||
let input = r#"
|
||||
### instance-method `format`
|
||||
|
||||
---
|
||||
→ `void`
|
||||
Parameters:
|
||||
- `const int &`
|
||||
- `const std::tm &`
|
||||
- `int & dest`
|
||||
|
||||
---
|
||||
```cpp
|
||||
// In my_formatter_flag
|
||||
public: void format(const int &, const std::tm &, int &dest)
|
||||
```
|
||||
"#;
|
||||
|
||||
let mut options = pulldown_cmark::Options::all();
|
||||
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
|
||||
|
||||
let parser = pulldown_cmark::Parser::new_ext(input, options);
|
||||
for event in parser.into_iter() {
|
||||
println!("{:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,6 @@ use text::*;
|
||||
|
||||
pub use proto::{BufferState, Operation};
|
||||
|
||||
/// Serializes a [`RopeFingerprint`] to be sent over RPC.
|
||||
pub fn serialize_fingerprint(fingerprint: RopeFingerprint) -> String {
|
||||
fingerprint.to_hex()
|
||||
}
|
||||
|
||||
/// Deserializes a `[text::LineEnding]` from the RPC representation.
|
||||
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
|
||||
match message {
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::Location;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AppContext;
|
||||
use task::{TaskTemplates, TaskVariables, VariableName};
|
||||
use text::{Point, ToPoint};
|
||||
|
||||
/// Language Contexts are used by Zed tasks to extract information about source file.
|
||||
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
|
||||
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
|
||||
///
|
||||
/// The context will be used to fill data for the tasks, and filter out the ones that do not have the variables required.
|
||||
pub trait ContextProvider: Send + Sync {
|
||||
fn build_context(&self, _: Location, _: &mut AppContext) -> Result<TaskVariables> {
|
||||
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
|
||||
fn build_context(
|
||||
&self,
|
||||
_: Option<&Path>,
|
||||
_: &Location,
|
||||
_: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
Ok(TaskVariables::default())
|
||||
}
|
||||
|
||||
/// Provides all tasks, associated with the current language.
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
None
|
||||
}
|
||||
|
||||
// Determines whether the [`BasicContextProvider`] variables should be filled too (if `false`), or omitted (if `true`).
|
||||
fn is_basic(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A context provider that finds out what symbol is currently focused in the buffer.
|
||||
pub struct SymbolContextProvider;
|
||||
/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
|
||||
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
|
||||
pub struct BasicContextProvider;
|
||||
|
||||
impl ContextProvider for BasicContextProvider {
|
||||
fn is_basic(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl ContextProvider for SymbolContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
location: Location,
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut AppContext,
|
||||
) -> gpui::Result<TaskVariables> {
|
||||
let symbols = location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.symbols_containing(location.range.start, None);
|
||||
) -> Result<TaskVariables> {
|
||||
let buffer = location.buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
|
||||
let symbol = symbols.unwrap_or_default().last().map(|symbol| {
|
||||
let range = symbol
|
||||
.name_ranges
|
||||
@@ -37,9 +59,40 @@ impl ContextProvider for SymbolContextProvider {
|
||||
.unwrap_or(0..symbol.text.len());
|
||||
symbol.text[range].to_string()
|
||||
});
|
||||
Ok(TaskVariables::from_iter(
|
||||
Some(VariableName::Symbol).zip(symbol),
|
||||
))
|
||||
|
||||
let current_file = buffer
|
||||
.file()
|
||||
.and_then(|file| file.as_local())
|
||||
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
|
||||
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
|
||||
let row = row + 1;
|
||||
let column = column + 1;
|
||||
let selected_text = buffer
|
||||
.chars_for_range(location.range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::Row, row.to_string()),
|
||||
(VariableName::Column, column.to_string()),
|
||||
]);
|
||||
|
||||
if let Some(symbol) = symbol {
|
||||
task_variables.insert(VariableName::Symbol, symbol);
|
||||
}
|
||||
if !selected_text.trim().is_empty() {
|
||||
task_variables.insert(VariableName::SelectedText, selected_text);
|
||||
}
|
||||
if let Some(path) = current_file {
|
||||
task_variables.insert(VariableName::File, path);
|
||||
}
|
||||
if let Some(worktree_path) = worktree_abs_path {
|
||||
task_variables.insert(
|
||||
VariableName::WorktreeRoot,
|
||||
worktree_path.to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(task_variables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +114,12 @@ impl ContextProvider for ContextProviderWithTasks {
|
||||
Some(self.templates.clone())
|
||||
}
|
||||
|
||||
fn build_context(&self, location: Location, cx: &mut AppContext) -> Result<TaskVariables> {
|
||||
SymbolContextProvider.build_context(location, cx)
|
||||
fn build_context(
|
||||
&self,
|
||||
worktree_abs_path: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
BasicContextProvider.build_context(worktree_abs_path, location, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
@@ -45,19 +44,16 @@ tree-sitter-embedded-template.workspace = true
|
||||
tree-sitter-go.workspace = true
|
||||
tree-sitter-gomod.workspace = true
|
||||
tree-sitter-gowork.workspace = true
|
||||
tree-sitter-hcl.workspace = true
|
||||
tree-sitter-heex.workspace = true
|
||||
tree-sitter-jsdoc.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter-markdown.workspace = true
|
||||
tree-sitter-nu.workspace = true
|
||||
tree-sitter-proto.workspace = true
|
||||
tree-sitter-python.workspace = true
|
||||
tree-sitter-regex.workspace = true
|
||||
tree-sitter-ruby.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
tree-sitter-vue.workspace = true
|
||||
tree-sitter-yaml.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
18
crates/languages/src/bash.rs
Normal file
18
crates/languages/src/bash.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use language::ContextProviderWithTasks;
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
|
||||
pub(super) fn bash_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: VariableName::SelectedText.template_value(),
|
||||
ignore_previously_resolved: true,
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("run '{}'", VariableName::File.template_value()),
|
||||
command: VariableName::File.template_value(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
@@ -4,6 +4,8 @@ use futures::StreamExt;
|
||||
use gpui::AsyncAppContext;
|
||||
pub use language::*;
|
||||
use lsp::LanguageServerBinary;
|
||||
use project::project_settings::{BinarySettings, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
|
||||
use util::{
|
||||
@@ -14,10 +16,51 @@ use util::{
|
||||
|
||||
pub struct CLspAdapter;
|
||||
|
||||
impl CLspAdapter {
|
||||
const SERVER_NAME: &'static str = "clangd";
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl super::LspAdapter for CLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("clangd".into())
|
||||
LanguageServerName(Self::SERVER_NAME.into())
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let configured_binary = cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(Self::SERVER_NAME)
|
||||
.and_then(|s| s.binary.clone())
|
||||
});
|
||||
|
||||
if let Ok(Some(BinarySettings {
|
||||
path: Some(path),
|
||||
arguments,
|
||||
})) = configured_binary
|
||||
{
|
||||
Some(LanguageServerBinary {
|
||||
path: path.into(),
|
||||
arguments: arguments
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|arg| arg.into())
|
||||
.collect(),
|
||||
env: None,
|
||||
})
|
||||
} else {
|
||||
let env = delegate.shell_env().await;
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec![],
|
||||
env: Some(env),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
@@ -45,20 +88,6 @@ impl super::LspAdapter for CLspAdapter {
|
||||
Ok(Box::new(version) as Box<_>)
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
_: &AsyncAppContext,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let env = delegate.shell_env().await;
|
||||
let path = delegate.which("clangd".as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec![],
|
||||
env: Some(env),
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
|
||||
@@ -88,7 +88,7 @@ impl super::LspAdapter for GoLspAdapter {
|
||||
})
|
||||
} else {
|
||||
let env = delegate.shell_env().await;
|
||||
let path = delegate.which("gopls".as_ref()).await?;
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
arguments: server_binary_arguments(),
|
||||
|
||||
@@ -8,24 +8,25 @@ use smol::stream::StreamExt;
|
||||
use std::{str, sync::Arc};
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
use crate::{elixir::elixir_task_context, rust::RustContextProvider};
|
||||
use crate::{
|
||||
bash::bash_task_context, elixir::elixir_task_context, python::python_task_context,
|
||||
rust::RustContextProvider,
|
||||
};
|
||||
|
||||
use self::{deno::DenoSettings, elixir::ElixirSettings};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
mod css;
|
||||
mod deno;
|
||||
mod elixir;
|
||||
mod go;
|
||||
mod json;
|
||||
mod nu;
|
||||
mod python;
|
||||
mod ruby;
|
||||
mod rust;
|
||||
mod tailwind;
|
||||
mod terraform;
|
||||
mod typescript;
|
||||
mod vue;
|
||||
mod yaml;
|
||||
|
||||
// 1. Add tree-sitter-{language} parser to zed crate
|
||||
@@ -63,12 +64,10 @@ pub fn init(
|
||||
("go", tree_sitter_go::language()),
|
||||
("gomod", tree_sitter_gomod::language()),
|
||||
("gowork", tree_sitter_gowork::language()),
|
||||
("hcl", tree_sitter_hcl::language()),
|
||||
("heex", tree_sitter_heex::language()),
|
||||
("jsdoc", tree_sitter_jsdoc::language()),
|
||||
("json", tree_sitter_json::language()),
|
||||
("markdown", tree_sitter_markdown::language()),
|
||||
("nu", tree_sitter_nu::language()),
|
||||
("proto", tree_sitter_proto::language()),
|
||||
("python", tree_sitter_python::language()),
|
||||
("regex", tree_sitter_regex::language()),
|
||||
@@ -76,7 +75,6 @@ pub fn init(
|
||||
("rust", tree_sitter_rust::language()),
|
||||
("tsx", tree_sitter_typescript::language_tsx()),
|
||||
("typescript", tree_sitter_typescript::language_typescript()),
|
||||
("vue", tree_sitter_vue::language()),
|
||||
("yaml", tree_sitter_yaml::language()),
|
||||
]);
|
||||
|
||||
@@ -91,7 +89,7 @@ pub fn init(
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::SymbolContextProvider)),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
);
|
||||
@@ -111,7 +109,7 @@ pub fn init(
|
||||
Ok((
|
||||
config.clone(),
|
||||
load_queries($name),
|
||||
Some(Arc::new(language::SymbolContextProvider)),
|
||||
Some(Arc::new(language::BasicContextProvider)),
|
||||
))
|
||||
},
|
||||
);
|
||||
@@ -137,7 +135,7 @@ pub fn init(
|
||||
);
|
||||
};
|
||||
}
|
||||
language!("bash");
|
||||
language!("bash", Vec::new(), bash_task_context());
|
||||
language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
|
||||
language!("cpp", vec![Arc::new(c::CLspAdapter)]);
|
||||
language!(
|
||||
@@ -199,7 +197,8 @@ pub fn init(
|
||||
"python",
|
||||
vec![Arc::new(python::PythonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))]
|
||||
))],
|
||||
python_task_context()
|
||||
);
|
||||
language!(
|
||||
"rust",
|
||||
@@ -271,21 +270,7 @@ pub fn init(
|
||||
"yaml",
|
||||
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
|
||||
);
|
||||
language!("nu", vec![Arc::new(nu::NuLanguageServer {})]);
|
||||
language!(
|
||||
"vue",
|
||||
vec![
|
||||
Arc::new(vue::VueLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
]
|
||||
);
|
||||
language!("proto");
|
||||
language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
|
||||
language!(
|
||||
"terraform-vars",
|
||||
vec![Arc::new(terraform::TerraformLspAdapter)]
|
||||
);
|
||||
language!("hcl", vec![]);
|
||||
|
||||
languages.register_secondary_lsp_adapter(
|
||||
"Astro".into(),
|
||||
@@ -303,6 +288,10 @@ pub fn init(
|
||||
"Svelte".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
);
|
||||
languages.register_secondary_lsp_adapter(
|
||||
"Vue.js".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
);
|
||||
|
||||
let mut subscription = languages.subscribe();
|
||||
let mut prev_language_settings = languages.language_settings();
|
||||
|
||||
@@ -20,5 +20,28 @@
|
||||
(info_string
|
||||
(language) @text.literal))
|
||||
|
||||
; hack to deal with incorrect grammar parsing
|
||||
(atx_heading
|
||||
(block_quote_marker) @punctuation.block_quote_marker
|
||||
)
|
||||
|
||||
; hack to deal with incorrect grammar parsing
|
||||
(paragraph
|
||||
(block_quote_marker) @punctuation.block_quote_marker
|
||||
)
|
||||
|
||||
; hack to deal with incorrect grammar parsing
|
||||
(list_item
|
||||
(block_quote_marker) @punctuation.block_quote_marker
|
||||
)
|
||||
|
||||
(block_quote
|
||||
(block_quote_marker) @punctuation.block_quote_marker
|
||||
)
|
||||
|
||||
(block_quote
|
||||
(paragraph) @text.block_quote
|
||||
)
|
||||
|
||||
(link_destination) @link_uri
|
||||
(link_text) @link_text
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use std::{any::Any, path::PathBuf};
|
||||
|
||||
pub struct NuLanguageServer;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for NuLanguageServer {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("nu".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(()))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
_version: Box<dyn 'static + Send + Any>,
|
||||
_container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
Err(anyhow!(
|
||||
"nu v0.87.0 or greater must be installed and available in your $PATH"
|
||||
))
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
_: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
Some(LanguageServerBinary {
|
||||
path: "nu".into(),
|
||||
env: None,
|
||||
arguments: vec!["--lsp".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
(parameter_pipes "|" @open "|" @close)
|
||||
@@ -1,10 +0,0 @@
|
||||
name = "Nu"
|
||||
grammar = "nu"
|
||||
path_suffixes = ["nu"]
|
||||
line_comments = ["# "]
|
||||
autoclose_before = ";:.,=}])>` \n\t\""
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
]
|
||||
@@ -1,284 +0,0 @@
|
||||
;;; ---
|
||||
;;; keywords
|
||||
[
|
||||
"def"
|
||||
"alias"
|
||||
"export-env"
|
||||
"export"
|
||||
"extern"
|
||||
"module"
|
||||
|
||||
"let"
|
||||
"let-env"
|
||||
"mut"
|
||||
"const"
|
||||
|
||||
"hide-env"
|
||||
|
||||
"source"
|
||||
"source-env"
|
||||
|
||||
"overlay"
|
||||
"register"
|
||||
|
||||
"loop"
|
||||
"while"
|
||||
"error"
|
||||
|
||||
"do"
|
||||
"if"
|
||||
"else"
|
||||
"try"
|
||||
"catch"
|
||||
"match"
|
||||
|
||||
"break"
|
||||
"continue"
|
||||
"return"
|
||||
|
||||
] @keyword
|
||||
|
||||
(hide_mod "hide" @keyword)
|
||||
(decl_use "use" @keyword)
|
||||
|
||||
(ctrl_for
|
||||
"for" @keyword
|
||||
"in" @keyword
|
||||
)
|
||||
(overlay_list "list" @keyword.storage.modifier)
|
||||
(overlay_hide "hide" @keyword.storage.modifier)
|
||||
(overlay_new "new" @keyword.storage.modifier)
|
||||
(overlay_use
|
||||
"use" @keyword.storage.modifier
|
||||
"as" @keyword
|
||||
)
|
||||
(ctrl_error "make" @keyword.storage.modifier)
|
||||
|
||||
;;; ---
|
||||
;;; literals
|
||||
(val_number) @constant.numeric
|
||||
(val_duration
|
||||
unit: [
|
||||
"ns" "µs" "us" "ms" "sec" "min" "hr" "day" "wk"
|
||||
] @variable.parameter
|
||||
)
|
||||
(val_filesize
|
||||
unit: [
|
||||
"b" "B"
|
||||
|
||||
"kb" "kB" "Kb" "KB"
|
||||
"mb" "mB" "Mb" "MB"
|
||||
"gb" "gB" "Gb" "GB"
|
||||
"tb" "tB" "Tb" "TB"
|
||||
"pb" "pB" "Pb" "PB"
|
||||
"eb" "eB" "Eb" "EB"
|
||||
|
||||
"kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
|
||||
"mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
|
||||
"gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
|
||||
"tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
|
||||
"pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
|
||||
"eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
|
||||
] @variable.parameter
|
||||
)
|
||||
(val_binary
|
||||
[
|
||||
"0b"
|
||||
"0o"
|
||||
"0x"
|
||||
] @constant.numeric
|
||||
"[" @punctuation.bracket
|
||||
digit: [
|
||||
"," @punctuation.delimiter
|
||||
(hex_digit) @constant.number
|
||||
]
|
||||
"]" @punctuation.bracket
|
||||
) @constant.numeric
|
||||
(val_bool) @constant.builtin
|
||||
(val_nothing) @constant.builtin
|
||||
(val_string) @string
|
||||
(val_date) @constant.number
|
||||
(inter_escape_sequence) @constant.character.escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
(val_interpolated [
|
||||
"$\""
|
||||
"$\'"
|
||||
"\""
|
||||
"\'"
|
||||
] @string)
|
||||
(unescaped_interpolated_content) @string
|
||||
(escaped_interpolated_content) @string
|
||||
(expr_interpolated ["(" ")"] @variable.parameter)
|
||||
|
||||
;;; ---
|
||||
;;; operators
|
||||
(expr_binary [
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"mod"
|
||||
"//"
|
||||
"++"
|
||||
"**"
|
||||
"=="
|
||||
"!="
|
||||
"<"
|
||||
"<="
|
||||
">"
|
||||
">="
|
||||
"=~"
|
||||
"!~"
|
||||
"and"
|
||||
"or"
|
||||
"xor"
|
||||
"bit-or"
|
||||
"bit-xor"
|
||||
"bit-and"
|
||||
"bit-shl"
|
||||
"bit-shr"
|
||||
"in"
|
||||
"not-in"
|
||||
"starts-with"
|
||||
"ends-with"
|
||||
] @operator )
|
||||
|
||||
(where_command [
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"mod"
|
||||
"//"
|
||||
"++"
|
||||
"**"
|
||||
"=="
|
||||
"!="
|
||||
"<"
|
||||
"<="
|
||||
">"
|
||||
">="
|
||||
"=~"
|
||||
"!~"
|
||||
"and"
|
||||
"or"
|
||||
"xor"
|
||||
"bit-or"
|
||||
"bit-xor"
|
||||
"bit-and"
|
||||
"bit-shl"
|
||||
"bit-shr"
|
||||
"in"
|
||||
"not-in"
|
||||
"starts-with"
|
||||
"ends-with"
|
||||
] @operator)
|
||||
|
||||
(assignment [
|
||||
"="
|
||||
"+="
|
||||
"-="
|
||||
"*="
|
||||
"/="
|
||||
"++="
|
||||
] @operator)
|
||||
|
||||
(expr_unary ["not" "-"] @operator)
|
||||
|
||||
(val_range [
|
||||
".."
|
||||
"..="
|
||||
"..<"
|
||||
] @operator)
|
||||
|
||||
["=>" "=" "|"] @operator
|
||||
|
||||
[
|
||||
"o>" "out>"
|
||||
"e>" "err>"
|
||||
"e+o>" "err+out>"
|
||||
"o+e>" "out+err>"
|
||||
] @special
|
||||
|
||||
;;; ---
|
||||
;;; punctuation
|
||||
[
|
||||
","
|
||||
";"
|
||||
] @punctuation.delimiter
|
||||
|
||||
(param_short_flag "-" @punctuation.delimiter)
|
||||
(param_long_flag ["--"] @punctuation.delimiter)
|
||||
(long_flag ["--"] @punctuation.delimiter)
|
||||
(param_rest "..." @punctuation.delimiter)
|
||||
(param_type [":"] @punctuation.special)
|
||||
(param_value ["="] @punctuation.special)
|
||||
(param_cmd ["@"] @punctuation.special)
|
||||
(param_opt ["?"] @punctuation.special)
|
||||
|
||||
[
|
||||
"(" ")"
|
||||
"{" "}"
|
||||
"[" "]"
|
||||
] @punctuation.bracket
|
||||
|
||||
(val_record
|
||||
(record_entry ":" @punctuation.delimiter))
|
||||
;;; ---
|
||||
;;; identifiers
|
||||
(param_rest
|
||||
name: (_) @variable.parameter)
|
||||
(param_opt
|
||||
name: (_) @variable.parameter)
|
||||
(parameter
|
||||
param_name: (_) @variable.parameter)
|
||||
(param_cmd
|
||||
(cmd_identifier) @string)
|
||||
(param_long_flag) @variable.parameter
|
||||
(param_short_flag) @variable.parameter
|
||||
|
||||
(short_flag) @variable.parameter
|
||||
(long_flag) @variable.parameter
|
||||
|
||||
(scope_pattern [(wild_card) @function])
|
||||
|
||||
(cmd_identifier) @function
|
||||
|
||||
(command
|
||||
"^" @punctuation.delimiter
|
||||
head: (_) @function
|
||||
)
|
||||
|
||||
"where" @function
|
||||
|
||||
(path
|
||||
["." "?"] @punctuation.delimiter
|
||||
) @variable.parameter
|
||||
|
||||
(val_variable
|
||||
"$" @variable.parameter
|
||||
[
|
||||
(identifier) @namespace
|
||||
"in"
|
||||
"nu"
|
||||
"env"
|
||||
"nothing"
|
||||
] @special
|
||||
)
|
||||
;;; ---
|
||||
;;; types
|
||||
(flat_type) @type.builtin
|
||||
(list_type
|
||||
"list" @type.enum
|
||||
["<" ">"] @punctuation.bracket
|
||||
)
|
||||
(collection_type
|
||||
["record" "table"] @type.enum
|
||||
"<" @punctuation.bracket
|
||||
key: (_) @variable.parameter
|
||||
["," ":"] @punctuation.delimiter
|
||||
">" @punctuation.bracket
|
||||
)
|
||||
|
||||
(shebang) @comment
|
||||
(comment) @comment
|
||||
@@ -1,3 +0,0 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{ContextProviderWithTasks, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use std::{
|
||||
@@ -9,6 +9,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::ResultExt;
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
|
||||
@@ -180,6 +181,30 @@ async fn get_cached_server_binary(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn python_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "execute selection".to_owned(),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![
|
||||
"-c".to_owned(),
|
||||
format!(
|
||||
"exec(r'''{}''')",
|
||||
VariableName::SelectedText.template_value()
|
||||
),
|
||||
],
|
||||
ignore_previously_resolved: true,
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!("run '{}'", VariableName::File.template_value()),
|
||||
command: "python3".to_owned(),
|
||||
args: vec![VariableName::File.template_value()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
||||
|
||||
@@ -331,25 +331,26 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName =
|
||||
impl ContextProvider for RustContextProvider {
|
||||
fn build_context(
|
||||
&self,
|
||||
location: Location,
|
||||
_: Option<&Path>,
|
||||
location: &Location,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Result<TaskVariables> {
|
||||
let mut context = SymbolContextProvider.build_context(location.clone(), cx)?;
|
||||
|
||||
let local_abs_path = location
|
||||
.buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| Some(file.as_local()?.abs_path(cx)));
|
||||
if let Some(package_name) = local_abs_path
|
||||
.as_deref()
|
||||
.and_then(|local_abs_path| local_abs_path.parent())
|
||||
.and_then(human_readable_package_name)
|
||||
{
|
||||
context.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
|
||||
}
|
||||
|
||||
Ok(context)
|
||||
Ok(
|
||||
if let Some(package_name) = local_abs_path
|
||||
.as_deref()
|
||||
.and_then(|local_abs_path| local_abs_path.parent())
|
||||
.and_then(human_readable_package_name)
|
||||
{
|
||||
TaskVariables::from_iter(Some((RUST_PACKAGE_TASK_VARIABLE.clone(), package_name)))
|
||||
} else {
|
||||
TaskVariables::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn associated_tasks(&self) -> Option<TaskTemplates> {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
pub use language::*;
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, ffi::OsString, path::PathBuf};
|
||||
use util::{
|
||||
fs::remove_matching,
|
||||
github::{latest_github_release, GitHubLspBinaryVersion},
|
||||
maybe, ResultExt,
|
||||
};
|
||||
|
||||
fn terraform_ls_binary_arguments() -> Vec<OsString> {
|
||||
vec!["serve".into()]
|
||||
}
|
||||
|
||||
pub struct TerraformLspAdapter;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for TerraformLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("terraform-ls".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
// TODO: maybe use release API instead
|
||||
// https://api.releases.hashicorp.com/v1/releases/terraform-ls?limit=1
|
||||
let release = latest_github_release(
|
||||
"hashicorp/terraform-ls",
|
||||
false,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Box::new(GitHubLspBinaryVersion {
|
||||
name: release.tag_name,
|
||||
url: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
container_dir: PathBuf,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let zip_path = container_dir.join(format!("terraform-ls_{}.zip", version.name));
|
||||
let version_dir = container_dir.join(format!("terraform-ls_{}", version.name));
|
||||
let binary_path = version_dir.join("terraform-ls");
|
||||
let url = build_download_url(version.name)?;
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
let mut response = delegate
|
||||
.http_client()
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.context("error downloading release")?;
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
futures::io::copy(response.body_mut(), &mut file).await?;
|
||||
|
||||
let unzip_status = smol::process::Command::new("unzip")
|
||||
.current_dir(&container_dir)
|
||||
.arg(&zip_path)
|
||||
.arg("-d")
|
||||
.arg(&version_dir)
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip Terraform LS archive"))?;
|
||||
}
|
||||
|
||||
remove_matching(&container_dir, |entry| entry != version_dir).await;
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: binary_path,
|
||||
env: None,
|
||||
arguments: terraform_ls_binary_arguments(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["version".into()];
|
||||
binary
|
||||
})
|
||||
}
|
||||
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
// TODO: file issue for server supported code actions
|
||||
// TODO: reenable default actions / delete override
|
||||
Some(vec![])
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
HashMap::from_iter([
|
||||
("Terraform".into(), "terraform".into()),
|
||||
("Terraform Vars".into(), "terraform-vars".into()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn build_download_url(version: String) -> Result<String> {
|
||||
let v = version.strip_prefix('v').unwrap_or(&version);
|
||||
let os = match std::env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"macos" => "darwin",
|
||||
"win" => "windows",
|
||||
_ => Err(anyhow!("unsupported OS {}", std::env::consts::OS))?,
|
||||
}
|
||||
.to_string();
|
||||
let arch = match std::env::consts::ARCH {
|
||||
"x86" => "386",
|
||||
"x86_64" => "amd64",
|
||||
"arm" => "arm",
|
||||
"aarch64" => "arm64",
|
||||
_ => Err(anyhow!("unsupported ARCH {}", std::env::consts::ARCH))?,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let url = format!(
|
||||
"https://releases.hashicorp.com/terraform-ls/{v}/terraform-ls_{v}_{os}_{arch}.zip",
|
||||
);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
|
||||
match last {
|
||||
Some(path) if path.is_dir() => {
|
||||
let binary = path.join("terraform-ls");
|
||||
if fs::metadata(&binary).await.is_ok() {
|
||||
return Ok(LanguageServerBinary {
|
||||
path: binary,
|
||||
env: None,
|
||||
arguments: terraform_ls_binary_arguments(),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Err(anyhow!("no cached binary"))
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
pub use language::*;
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use smol::fs::{self};
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
|
||||
pub struct VueLspVersion {
|
||||
vue_version: String,
|
||||
ts_version: String,
|
||||
}
|
||||
|
||||
pub struct VueLspAdapter {
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
typescript_install_path: Mutex<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
impl VueLspAdapter {
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/@vue/language-server/bin/vue-language-server.js";
|
||||
// TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
|
||||
const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
let typescript_install_path = Mutex::new(None);
|
||||
Self {
|
||||
node,
|
||||
typescript_install_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait(?Send)]
|
||||
impl super::LspAdapter for VueLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vue-language-server".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
Ok(Box::new(VueLspVersion {
|
||||
// We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
|
||||
vue_version: "1.8".to_string(),
|
||||
ts_version: self.node.npm_package_latest_version("typescript").await?,
|
||||
}) as Box<_>)
|
||||
}
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let typescript_sdk_path = self.typescript_install_path.lock();
|
||||
let typescript_sdk_path = typescript_sdk_path
|
||||
.as_ref()
|
||||
.expect("initialization_options called without a container_dir for typescript");
|
||||
|
||||
Ok(Some(serde_json::json!({
|
||||
"typescript": {
|
||||
"tsdk": typescript_sdk_path
|
||||
}
|
||||
})))
|
||||
}
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
// REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
|
||||
// sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
|
||||
Some(vec![
|
||||
CodeActionKind::EMPTY,
|
||||
CodeActionKind::QUICKFIX,
|
||||
CodeActionKind::REFACTOR_REWRITE,
|
||||
])
|
||||
}
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<VueLspVersion>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
|
||||
|
||||
let vue_package_name = "@vue/language-server";
|
||||
let should_install_vue_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
vue_package_name,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
&latest_version.vue_version,
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_vue_language_server {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(vue_package_name, latest_version.vue_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
ensure!(
|
||||
fs::metadata(&server_path).await.is_ok(),
|
||||
"@vue/language-server package installation failed"
|
||||
);
|
||||
|
||||
let ts_package_name = "typescript";
|
||||
let should_install_ts_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(
|
||||
ts_package_name,
|
||||
&server_path,
|
||||
&container_dir,
|
||||
&latest_version.ts_version,
|
||||
)
|
||||
.await;
|
||||
|
||||
if should_install_ts_language_server {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[(ts_package_name, latest_version.ts_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
ensure!(
|
||||
fs::metadata(&ts_path).await.is_ok(),
|
||||
"typescript for Vue package installation failed"
|
||||
);
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: vue_server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Some(server)
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
|
||||
.await
|
||||
.map(|(mut binary, ts_path)| {
|
||||
binary.arguments = vec!["--help".into()];
|
||||
(binary, ts_path)
|
||||
})?;
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Some(server)
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
|
||||
Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
|
||||
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
|
||||
Kind::VARIABLE => grammar.highlight_id_for_name("type"),
|
||||
Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
|
||||
Kind::VALUE => grammar.highlight_id_for_name("tag"),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let text = match &item.detail {
|
||||
Some(detail) => format!("{} {}", item.label, detail),
|
||||
None => item.label.clone(),
|
||||
};
|
||||
|
||||
Some(language::CodeLabel {
|
||||
text,
|
||||
runs: vec![(0..len, highlight_id)],
|
||||
filter_range: 0..len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
type TypescriptPath = PathBuf;
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
) -> Option<(LanguageServerBinary, TypescriptPath)> {
|
||||
maybe!(async {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
|
||||
let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
|
||||
if server_path.exists() && typescript_path.exists() {
|
||||
Ok((
|
||||
LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: vue_server_binary_arguments(&server_path),
|
||||
},
|
||||
typescript_path,
|
||||
))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
@@ -138,8 +138,16 @@ struct AnyResponse<'a> {
|
||||
struct Response<T> {
|
||||
jsonrpc: &'static str,
|
||||
id: RequestId,
|
||||
result: Option<T>,
|
||||
error: Option<Error>,
|
||||
#[serde(flatten)]
|
||||
value: LspResult<T>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum LspResult<T> {
|
||||
#[serde(rename = "result")]
|
||||
Ok(Option<T>),
|
||||
Error(Option<Error>),
|
||||
}
|
||||
|
||||
/// Language server protocol RPC notification message.
|
||||
@@ -867,16 +875,14 @@ impl LanguageServer {
|
||||
Ok(result) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
value: LspResult::Ok(Some(result)),
|
||||
},
|
||||
Err(error) => Response {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
id,
|
||||
result: None,
|
||||
error: Some(Error {
|
||||
value: LspResult::Error(Some(Error {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
};
|
||||
if let Some(response) =
|
||||
@@ -1503,4 +1509,27 @@ mod tests {
|
||||
let expected_id = RequestId::Int(2);
|
||||
assert_eq!(notification.id, Some(expected_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_has_no_nulls() {
|
||||
// Ensure we're not setting both result and error variants. (ticket #10595)
|
||||
let no_tag = Response::<u32> {
|
||||
jsonrpc: "",
|
||||
id: RequestId::Int(0),
|
||||
value: LspResult::Ok(None),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&no_tag).unwrap(),
|
||||
"{\"jsonrpc\":\"\",\"id\":0,\"result\":null}"
|
||||
);
|
||||
let no_tag = Response::<u32> {
|
||||
jsonrpc: "",
|
||||
id: RequestId::Int(0),
|
||||
value: LspResult::Error(None),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_string(&no_tag).unwrap(),
|
||||
"{\"jsonrpc\":\"\",\"id\":0,\"error\":null}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::{convert::TryFrom, future::Future};
|
||||
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
|
||||
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
@@ -188,3 +190,68 @@ pub async fn stream_completion(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum OpenAiEmbeddingModel {
|
||||
#[serde(rename = "text-embedding-3-small")]
|
||||
TextEmbedding3Small,
|
||||
#[serde(rename = "text-embedding-3-large")]
|
||||
TextEmbedding3Large,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OpenAiEmbeddingRequest<'a> {
|
||||
model: OpenAiEmbeddingModel,
|
||||
input: Vec<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OpenAiEmbeddingResponse {
|
||||
pub data: Vec<OpenAiEmbedding>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OpenAiEmbedding {
|
||||
pub embedding: Vec<f32>,
|
||||
}
|
||||
|
||||
pub fn embed<'a>(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
model: OpenAiEmbeddingModel,
|
||||
texts: impl IntoIterator<Item = &'a str>,
|
||||
) -> impl 'static + Future<Output = Result<OpenAiEmbeddingResponse>> {
|
||||
let uri = format!("{api_url}/embeddings");
|
||||
|
||||
let request = OpenAiEmbeddingRequest {
|
||||
model,
|
||||
input: texts.into_iter().collect(),
|
||||
};
|
||||
let body = AsyncBody::from(serde_json::to_string(&request).unwrap());
|
||||
let request = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(body)
|
||||
.map(|request| client.send(request));
|
||||
|
||||
async move {
|
||||
let mut response = request?.await?;
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let response: OpenAiEmbeddingResponse =
|
||||
serde_json::from_str(&body).context("failed to parse OpenAI embedding response")?;
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"error during embedding, status: {:?}, body: {:?}",
|
||||
response.status(),
|
||||
body
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ use std::{
|
||||
};
|
||||
use task::static_source::{StaticSource, TrackedFile};
|
||||
use terminals::Terminals;
|
||||
use text::{Anchor, BufferId, RopeFingerprint};
|
||||
use text::{Anchor, BufferId};
|
||||
use util::{
|
||||
debug_panic, defer,
|
||||
http::{HttpClient, Url},
|
||||
@@ -978,6 +978,50 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn example(
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Model<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
let fs = Arc::new(RealFs::default());
|
||||
let languages = LanguageRegistry::test(cx.background_executor().clone());
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let client = cx
|
||||
.update(|cx| client::Client::new(clock, http_client.clone(), cx))
|
||||
.unwrap();
|
||||
let user_store = cx
|
||||
.new_model(|cx| UserStore::new(client.clone(), cx))
|
||||
.unwrap();
|
||||
let project = cx
|
||||
.update(|cx| {
|
||||
Project::local(
|
||||
client,
|
||||
node_runtime::FakeNodeRuntime::new(),
|
||||
user_store,
|
||||
Arc::new(languages),
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree(path, true, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.unwrap()
|
||||
.await;
|
||||
}
|
||||
project
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn test(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -1146,6 +1190,10 @@ impl Project {
|
||||
self.user_store.clone()
|
||||
}
|
||||
|
||||
pub fn node_runtime(&self) -> Option<&Arc<dyn NodeRuntime>> {
|
||||
self.node.as_ref()
|
||||
}
|
||||
|
||||
pub fn opened_buffers(&self) -> Vec<Model<Buffer>> {
|
||||
self.opened_buffers
|
||||
.values()
|
||||
@@ -7708,13 +7756,20 @@ impl Project {
|
||||
.as_local()
|
||||
.context("worktree was not local")?
|
||||
.snapshot();
|
||||
let (work_directory, repo) = worktree
|
||||
.repository_and_work_directory_for_path(&buffer_project_path.path)
|
||||
.context("failed to get repo for blamed buffer")?;
|
||||
|
||||
let repo_entry = worktree
|
||||
.get_local_repo(&repo)
|
||||
.context("failed to get repo for blamed buffer")?;
|
||||
let (work_directory, repo) = match worktree
|
||||
.repository_and_work_directory_for_path(&buffer_project_path.path)
|
||||
{
|
||||
Some(work_dir_repo) => work_dir_repo,
|
||||
None => anyhow::bail!(NoRepositoryError {}),
|
||||
};
|
||||
|
||||
let repo_entry = match worktree.get_local_repo(&repo) {
|
||||
Some(repo_entry) => repo_entry,
|
||||
None => anyhow::bail!(NoRepositoryError {}),
|
||||
};
|
||||
|
||||
let repo = repo_entry.repo().clone();
|
||||
|
||||
let relative_path = buffer_project_path
|
||||
.path
|
||||
@@ -7725,7 +7780,6 @@ impl Project {
|
||||
Some(version) => buffer.rope_for_version(&version).clone(),
|
||||
None => buffer.as_rope().clone(),
|
||||
};
|
||||
let repo = repo_entry.repo().clone();
|
||||
|
||||
anyhow::Ok((repo, relative_path, content))
|
||||
});
|
||||
@@ -7734,6 +7788,7 @@ impl Project {
|
||||
let (repo, relative_path, content) = blame_params?;
|
||||
let lock = repo.lock();
|
||||
lock.blame(&relative_path, content)
|
||||
.with_context(|| format!("Failed to blame {relative_path:?}"))
|
||||
})
|
||||
} else {
|
||||
let project_id = self.remote_id();
|
||||
@@ -8525,7 +8580,6 @@ impl Project {
|
||||
buffer_id: buffer_id.into(),
|
||||
version: serialize_version(buffer.saved_version()),
|
||||
mtime: buffer.saved_mtime().map(|time| time.into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8618,9 +8672,6 @@ impl Project {
|
||||
buffer_id: buffer_id.into(),
|
||||
version: language::proto::serialize_version(buffer.saved_version()),
|
||||
mtime: buffer.saved_mtime().map(|time| time.into()),
|
||||
fingerprint: language::proto::serialize_fingerprint(
|
||||
buffer.saved_version_fingerprint(),
|
||||
),
|
||||
line_ending: language::proto::serialize_line_ending(
|
||||
buffer.line_ending(),
|
||||
) as i32,
|
||||
@@ -9609,7 +9660,6 @@ impl Project {
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let fingerprint = Default::default();
|
||||
let version = deserialize_version(&envelope.payload.version);
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
let mtime = envelope.payload.mtime.map(|time| time.into());
|
||||
@@ -9622,7 +9672,7 @@ impl Project {
|
||||
.or_else(|| this.incomplete_remote_buffers.get(&buffer_id).cloned());
|
||||
if let Some(buffer) = buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
buffer.did_save(version, mtime, cx);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
@@ -9637,7 +9687,6 @@ impl Project {
|
||||
) -> Result<()> {
|
||||
let payload = envelope.payload;
|
||||
let version = deserialize_version(&payload.version);
|
||||
let fingerprint = RopeFingerprint::default();
|
||||
let line_ending = deserialize_line_ending(
|
||||
proto::LineEnding::from_i32(payload.line_ending)
|
||||
.ok_or_else(|| anyhow!("missing line ending"))?,
|
||||
@@ -9652,7 +9701,7 @@ impl Project {
|
||||
.or_else(|| this.incomplete_remote_buffers.get(&buffer_id).cloned());
|
||||
if let Some(buffer) = buffer {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.did_reload(version, fingerprint, line_ending, mtime, cx);
|
||||
buffer.did_reload(version, line_ending, mtime, cx);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
@@ -10739,3 +10788,14 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||
Some(hover)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoRepositoryError {}
|
||||
|
||||
impl std::fmt::Display for NoRepositoryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "no git repository for worktree found")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NoRepositoryError {}
|
||||
|
||||
@@ -3,7 +3,7 @@ use gpui::AppContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProjectSettings {
|
||||
@@ -29,6 +29,30 @@ pub struct GitSettings {
|
||||
/// Default: tracked_files
|
||||
pub git_gutter: Option<GitGutterSetting>,
|
||||
pub gutter_debounce: Option<u64>,
|
||||
/// Whether or not to show git blame data inline in
|
||||
/// the currently focused line.
|
||||
///
|
||||
/// Default: off
|
||||
pub inline_blame: Option<InlineBlameSettings>,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
pub fn inline_blame_enabled(&self) -> bool {
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings { enabled, .. }) => enabled,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_blame_delay(&self) -> Option<Duration> {
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings {
|
||||
delay_ms: Some(delay_ms),
|
||||
..
|
||||
}) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -41,6 +65,26 @@ pub enum GitGutterSetting {
|
||||
Hide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct InlineBlameSettings {
|
||||
/// Whether or not to show git blame data inline in
|
||||
/// the currently focused line.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "true_value")]
|
||||
pub enabled: bool,
|
||||
/// Whether to only show the inline blame information
|
||||
/// after a delay once the cursor stops moving.
|
||||
///
|
||||
/// Default: 0
|
||||
pub delay_ms: Option<u64>,
|
||||
}
|
||||
|
||||
const fn true_value() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct BinarySettings {
|
||||
pub path: Option<String>,
|
||||
|
||||
@@ -14,6 +14,7 @@ use serde_json::json;
|
||||
#[cfg(not(windows))]
|
||||
use std::os;
|
||||
use std::task::Poll;
|
||||
use task::{TaskContext, TaskSource, TaskTemplate, TaskTemplates};
|
||||
use unindent::Unindent as _;
|
||||
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
|
||||
use worktree::WorktreeModelHandle as _;
|
||||
@@ -125,8 +126,19 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||
|
||||
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
|
||||
let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
|
||||
let task_context = TaskContext::default();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let workree_id = cx.update(|cx| {
|
||||
project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
})
|
||||
});
|
||||
let global_task_source_kind = TaskSourceKind::Worktree {
|
||||
id: workree_id,
|
||||
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
|
||||
id_base: "local_tasks_for_worktree",
|
||||
};
|
||||
cx.update(|cx| {
|
||||
let tree = worktree.read(cx);
|
||||
|
||||
@@ -154,17 +166,29 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||
assert_eq!(settings_a.tab_size.get(), 8);
|
||||
assert_eq!(settings_b.tab_size.get(), 2);
|
||||
|
||||
let workree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
let all_tasks = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.task_inventory()
|
||||
.update(cx, |inventory, cx| inventory.list_tasks(None, None, cx))
|
||||
project.task_inventory().update(cx, |inventory, cx| {
|
||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
||||
None,
|
||||
Some(workree_id),
|
||||
&task_context,
|
||||
cx,
|
||||
);
|
||||
old.extend(new);
|
||||
old
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.map(|(source_kind, task)| (source_kind, task.label))
|
||||
.map(|(source_kind, task)| {
|
||||
let resolved = task.resolved.unwrap();
|
||||
(
|
||||
source_kind,
|
||||
task.resolved_label,
|
||||
resolved.args,
|
||||
resolved.env,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
all_tasks,
|
||||
@@ -172,24 +196,141 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: workree_id,
|
||||
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
|
||||
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
|
||||
id_base: "local_tasks_for_worktree",
|
||||
},
|
||||
"cargo check".to_string()
|
||||
"cargo check".to_string(),
|
||||
vec!["check".to_string()],
|
||||
HashMap::default(),
|
||||
),
|
||||
(
|
||||
global_task_source_kind.clone(),
|
||||
"cargo check".to_string(),
|
||||
vec!["check".to_string(), "--all".to_string()],
|
||||
HashMap::default(),
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let inventory = project.task_inventory();
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
||||
None,
|
||||
Some(workree_id),
|
||||
&task_context,
|
||||
cx,
|
||||
);
|
||||
old.extend(new);
|
||||
let (_, resolved_task) = old
|
||||
.into_iter()
|
||||
.find(|(source_kind, _)| source_kind == &global_task_source_kind)
|
||||
.expect("should have one global task");
|
||||
inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
|
||||
})
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
let all_tasks = project
|
||||
.update(cx, |project, cx| {
|
||||
project.task_inventory().update(cx, |inventory, cx| {
|
||||
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
|
||||
inventory.add_source(
|
||||
global_task_source_kind.clone(),
|
||||
|cx| {
|
||||
cx.new_model(|_| {
|
||||
let source = TestTaskSource {
|
||||
tasks: TaskTemplates(vec![TaskTemplate {
|
||||
label: "cargo check".to_string(),
|
||||
command: "cargo".to_string(),
|
||||
args: vec![
|
||||
"check".to_string(),
|
||||
"--all".to_string(),
|
||||
"--all-targets".to_string(),
|
||||
],
|
||||
env: HashMap::from_iter(Some((
|
||||
"RUSTFLAGS".to_string(),
|
||||
"-Zunstable-options".to_string(),
|
||||
))),
|
||||
..TaskTemplate::default()
|
||||
}]),
|
||||
};
|
||||
Box::new(source) as Box<_>
|
||||
})
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
||||
None,
|
||||
Some(workree_id),
|
||||
&task_context,
|
||||
cx,
|
||||
);
|
||||
old.extend(new);
|
||||
old
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
.map(|(source_kind, task)| {
|
||||
let resolved = task.resolved.unwrap();
|
||||
(
|
||||
source_kind,
|
||||
task.resolved_label,
|
||||
resolved.args,
|
||||
resolved.env,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
all_tasks,
|
||||
vec![
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: workree_id,
|
||||
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
|
||||
id_base: "local_tasks_for_worktree",
|
||||
},
|
||||
"cargo check".to_string()
|
||||
"cargo check".to_string(),
|
||||
vec!["check".to_string()],
|
||||
HashMap::default(),
|
||||
),
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: workree_id,
|
||||
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
|
||||
id_base: "local_tasks_for_worktree",
|
||||
},
|
||||
"cargo check".to_string(),
|
||||
vec![
|
||||
"check".to_string(),
|
||||
"--all".to_string(),
|
||||
"--all-targets".to_string()
|
||||
],
|
||||
HashMap::from_iter(Some((
|
||||
"RUSTFLAGS".to_string(),
|
||||
"-Zunstable-options".to_string()
|
||||
))),
|
||||
),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
struct TestTaskSource {
|
||||
tasks: TaskTemplates,
|
||||
}
|
||||
|
||||
impl TaskSource for TestTaskSource {
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
|
||||
self.tasks.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2661,7 +2802,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
worktree.next_event(cx).await;
|
||||
|
||||
// Change the buffer's file again. Depending on the random seed, the
|
||||
// previous file change may still be in progress.
|
||||
@@ -2672,7 +2813,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
worktree.next_event(cx).await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
|
||||
@@ -2716,7 +2857,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree.next_event(cx);
|
||||
worktree.next_event(cx).await;
|
||||
|
||||
cx.executor()
|
||||
.spawn(cx.executor().simulate_random_delay())
|
||||
@@ -3122,12 +3263,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
|
||||
&[language::Event::Edited, language::Event::DirtyChanged]
|
||||
);
|
||||
events.lock().clear();
|
||||
buffer.did_save(
|
||||
buffer.version(),
|
||||
buffer.as_rope().fingerprint(),
|
||||
buffer.file().unwrap().mtime(),
|
||||
cx,
|
||||
);
|
||||
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
|
||||
});
|
||||
|
||||
// after saving, the buffer is not dirty, and emits a saved event.
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
cmp::{self, Reverse},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use collections::{HashMap, VecDeque};
|
||||
use collections::{hash_map, HashMap, VecDeque};
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||
use itertools::{Either, Itertools};
|
||||
use language::Language;
|
||||
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate};
|
||||
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
|
||||
use util::{post_inc, NumericPrefixWithSuffix};
|
||||
use worktree::WorktreeId;
|
||||
|
||||
@@ -198,7 +198,7 @@ impl Inventory {
|
||||
&self,
|
||||
language: Option<Arc<Language>>,
|
||||
worktree: Option<WorktreeId>,
|
||||
task_context: TaskContext,
|
||||
task_context: &TaskContext,
|
||||
cx: &mut AppContext,
|
||||
) -> (
|
||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||
@@ -214,17 +214,22 @@ impl Inventory {
|
||||
.flat_map(|task| Some((task_source_kind.as_ref()?, task)));
|
||||
|
||||
let mut lru_score = 0_u32;
|
||||
let mut task_usage = self.last_scheduled_tasks.iter().rev().fold(
|
||||
HashMap::default(),
|
||||
|mut tasks, (task_source_kind, resolved_task)| {
|
||||
tasks
|
||||
.entry(&resolved_task.id)
|
||||
.or_insert_with(|| (task_source_kind, resolved_task, post_inc(&mut lru_score)));
|
||||
tasks
|
||||
},
|
||||
);
|
||||
let mut task_usage = self
|
||||
.last_scheduled_tasks
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|(_, task)| !task.original_task().ignore_previously_resolved)
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut tasks, (task_source_kind, resolved_task)| {
|
||||
tasks.entry(&resolved_task.id).or_insert_with(|| {
|
||||
(task_source_kind, resolved_task, post_inc(&mut lru_score))
|
||||
});
|
||||
tasks
|
||||
},
|
||||
);
|
||||
let not_used_score = post_inc(&mut lru_score);
|
||||
let current_resolved_tasks = self
|
||||
let currently_resolved_tasks = self
|
||||
.sources
|
||||
.iter()
|
||||
.filter(|source| {
|
||||
@@ -242,7 +247,7 @@ impl Inventory {
|
||||
.chain(language_tasks)
|
||||
.filter_map(|(kind, task)| {
|
||||
let id_base = kind.to_id_base();
|
||||
Some((kind, task.resolve_task(&id_base, task_context.clone())?))
|
||||
Some((kind, task.resolve_task(&id_base, task_context)?))
|
||||
})
|
||||
.map(|(kind, task)| {
|
||||
let lru_score = task_usage
|
||||
@@ -252,16 +257,55 @@ impl Inventory {
|
||||
(kind.clone(), task, lru_score)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let previous_resolved_tasks = task_usage
|
||||
let previously_spawned_tasks = task_usage
|
||||
.into_iter()
|
||||
.map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
|
||||
|
||||
previous_resolved_tasks
|
||||
.chain(current_resolved_tasks)
|
||||
let mut tasks_by_label = HashMap::default();
|
||||
tasks_by_label = previously_spawned_tasks.into_iter().fold(
|
||||
tasks_by_label,
|
||||
|mut tasks_by_label, (source, task, lru_score)| {
|
||||
match tasks_by_label.entry((source, task.resolved_label.clone())) {
|
||||
hash_map::Entry::Occupied(mut o) => {
|
||||
let (_, previous_lru_score) = o.get();
|
||||
if previous_lru_score >= &lru_score {
|
||||
o.insert((task, lru_score));
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert((task, lru_score));
|
||||
}
|
||||
}
|
||||
tasks_by_label
|
||||
},
|
||||
);
|
||||
tasks_by_label = currently_resolved_tasks.into_iter().fold(
|
||||
tasks_by_label,
|
||||
|mut tasks_by_label, (source, task, lru_score)| {
|
||||
match tasks_by_label.entry((source, task.resolved_label.clone())) {
|
||||
hash_map::Entry::Occupied(mut o) => {
|
||||
let (previous_task, _) = o.get();
|
||||
let new_template = task.original_task();
|
||||
if new_template.ignore_previously_resolved
|
||||
|| new_template != previous_task.original_task()
|
||||
{
|
||||
o.insert((task, lru_score));
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert((task, lru_score));
|
||||
}
|
||||
}
|
||||
tasks_by_label
|
||||
},
|
||||
);
|
||||
|
||||
tasks_by_label
|
||||
.into_iter()
|
||||
.map(|((kind, _), (task, lru_score))| (kind, task, lru_score))
|
||||
.sorted_unstable_by(task_lru_comparator)
|
||||
.unique_by(|(kind, task, _)| (kind.clone(), task.resolved_label.clone()))
|
||||
.partition_map(|(kind, task, lru_index)| {
|
||||
if lru_index < not_used_score {
|
||||
.partition_map(|(kind, task, lru_score)| {
|
||||
if lru_score < not_used_score {
|
||||
Either::Left((kind, task))
|
||||
} else {
|
||||
Either::Right((kind, task))
|
||||
@@ -299,22 +343,14 @@ fn task_lru_comparator(
|
||||
(kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
|
||||
) -> cmp::Ordering {
|
||||
lru_score_a
|
||||
// First, display recently used templates above all.
|
||||
.cmp(&lru_score_b)
|
||||
// Then, ensure more specific sources are displayed first.
|
||||
.then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
|
||||
.then(
|
||||
kind_a
|
||||
.worktree()
|
||||
.is_none()
|
||||
.cmp(&kind_b.worktree().is_none()),
|
||||
)
|
||||
.then(kind_a.worktree().cmp(&kind_b.worktree()))
|
||||
.then(
|
||||
kind_a
|
||||
.abs_path()
|
||||
.is_none()
|
||||
.cmp(&kind_b.abs_path().is_none()),
|
||||
)
|
||||
.then(kind_a.abs_path().cmp(&kind_b.abs_path()))
|
||||
// After that, display first more specific tasks, using more template variables.
|
||||
// Bonus points for tasks with symbol variables.
|
||||
.then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
|
||||
// Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
|
||||
.then({
|
||||
NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
|
||||
.cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
||||
@@ -333,6 +369,15 @@ fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
|
||||
let task_variables = task.substituted_variables();
|
||||
Reverse(if task_variables.contains(&VariableName::Symbol) {
|
||||
task_variables.len() + 1
|
||||
} else {
|
||||
task_variables.len()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_inventory {
|
||||
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
|
||||
@@ -421,12 +466,12 @@ mod test_inventory {
|
||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
||||
None,
|
||||
worktree,
|
||||
TaskContext::default(),
|
||||
&TaskContext::default(),
|
||||
cx,
|
||||
);
|
||||
used.into_iter()
|
||||
.chain(current)
|
||||
.map(|(_, task)| task.original_task.label)
|
||||
.map(|(_, task)| task.original_task().label.clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
@@ -445,7 +490,7 @@ mod test_inventory {
|
||||
let id_base = task_source_kind.to_id_base();
|
||||
inventory.task_scheduled(
|
||||
task_source_kind.clone(),
|
||||
task.resolve_task(&id_base, TaskContext::default())
|
||||
task.resolve_task(&id_base, &TaskContext::default())
|
||||
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
|
||||
);
|
||||
});
|
||||
@@ -460,7 +505,7 @@ mod test_inventory {
|
||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
||||
None,
|
||||
worktree,
|
||||
TaskContext::default(),
|
||||
&TaskContext::default(),
|
||||
cx,
|
||||
);
|
||||
let mut all = used;
|
||||
@@ -699,7 +744,7 @@ mod tests {
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: "test source",
|
||||
abs_path: path_1.to_path_buf(),
|
||||
abs_path: path_2.to_path_buf(),
|
||||
},
|
||||
common_name.to_string(),
|
||||
),
|
||||
@@ -713,7 +758,7 @@ mod tests {
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: "test source",
|
||||
abs_path: path_2.to_path_buf(),
|
||||
abs_path: path_1.to_path_buf(),
|
||||
},
|
||||
common_name.to_string(),
|
||||
),
|
||||
|
||||
@@ -44,6 +44,7 @@ impl Project {
|
||||
.unwrap_or_else(|| Path::new(""));
|
||||
|
||||
let (spawn_task, shell) = if let Some(spawn_task) = spawn_task {
|
||||
log::debug!("Spawning task: {spawn_task:?}");
|
||||
env.extend(spawn_task.env);
|
||||
// Activate minimal Python virtual environment
|
||||
if let Some(python_settings) = &python_settings.as_option() {
|
||||
@@ -52,7 +53,16 @@ impl Project {
|
||||
(
|
||||
Some(TaskState {
|
||||
id: spawn_task.id,
|
||||
full_label: spawn_task.full_label,
|
||||
label: spawn_task.label,
|
||||
command_label: spawn_task.args.iter().fold(
|
||||
spawn_task.command.clone(),
|
||||
|mut command_label, new_arg| {
|
||||
command_label.push(' ');
|
||||
command_label.push_str(new_arg);
|
||||
command_label
|
||||
},
|
||||
),
|
||||
status: TaskStatus::Running,
|
||||
completion_rx,
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod project_panel_settings;
|
||||
use client::{ErrorCode, ErrorExt};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor};
|
||||
@@ -24,6 +24,7 @@ use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
@@ -50,6 +51,7 @@ pub struct ProjectPanel {
|
||||
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
|
||||
last_worktree_root_id: Option<ProjectEntryId>,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
unfolded_dir_ids: HashSet<ProjectEntryId>,
|
||||
selection: Option<Selection>,
|
||||
context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
edit_state: Option<EditState>,
|
||||
@@ -133,6 +135,8 @@ actions!(
|
||||
OpenPermanent,
|
||||
ToggleFocus,
|
||||
NewSearchInDirectory,
|
||||
UnfoldDirectory,
|
||||
FoldDirectory,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -235,6 +239,16 @@ impl ProjectPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||
let new_settings = *ProjectPanelSettings::get_global(cx);
|
||||
if project_panel_settings != new_settings {
|
||||
project_panel_settings = new_settings;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
@@ -243,6 +257,7 @@ impl ProjectPanel {
|
||||
visible_entries: Default::default(),
|
||||
last_worktree_root_id: Default::default(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
unfolded_dir_ids: Default::default(),
|
||||
selection: None,
|
||||
edit_state: None,
|
||||
context_menu: None,
|
||||
@@ -403,8 +418,11 @@ impl ProjectPanel {
|
||||
});
|
||||
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
|
||||
let is_root = Some(entry) == worktree.root_entry();
|
||||
let is_dir = entry.is_dir();
|
||||
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
|
||||
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
|
||||
let worktree_id = worktree.id();
|
||||
let is_local = project.is_local();
|
||||
let is_read_only = project.is_read_only();
|
||||
@@ -430,6 +448,12 @@ impl ProjectPanel {
|
||||
menu.separator()
|
||||
.action("Find in Folder…", Box::new(NewSearchInDirectory))
|
||||
})
|
||||
.when(is_unfoldable, |menu| {
|
||||
menu.action("Unfold Directory", Box::new(UnfoldDirectory))
|
||||
})
|
||||
.when(is_foldable, |menu| {
|
||||
menu.action("Fold Directory", Box::new(FoldDirectory))
|
||||
})
|
||||
.separator()
|
||||
.action("Cut", Box::new(Cut))
|
||||
.action("Copy", Box::new(Copy))
|
||||
@@ -482,6 +506,37 @@ impl ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
|
||||
if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(parent_path) = entry.path.parent() {
|
||||
let snapshot = worktree.snapshot();
|
||||
let mut child_entries = snapshot.child_entries(&parent_path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if child_entries.next().is_none() {
|
||||
return child.kind.is_dir();
|
||||
}
|
||||
}
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
|
||||
if entry.is_dir() {
|
||||
let snapshot = worktree.snapshot();
|
||||
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if child_entries.next().is_none() {
|
||||
return child.kind.is_dir();
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
if entry.is_dir() {
|
||||
@@ -859,6 +914,59 @@ impl ProjectPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
self.unfolded_dir_ids.insert(entry.id);
|
||||
|
||||
let snapshot = worktree.snapshot();
|
||||
let mut parent_path = entry.path.parent();
|
||||
while let Some(path) = parent_path {
|
||||
if let Some(parent_entry) = worktree.entry_for_path(path) {
|
||||
let mut children_iter = snapshot.child_entries(path);
|
||||
|
||||
if children_iter.by_ref().take(2).count() > 1 {
|
||||
break;
|
||||
}
|
||||
|
||||
self.unfolded_dir_ids.insert(parent_entry.id);
|
||||
parent_path = path.parent();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visible_entries(None, cx);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
self.unfolded_dir_ids.remove(&entry.id);
|
||||
|
||||
let snapshot = worktree.snapshot();
|
||||
let mut path = &*entry.path;
|
||||
loop {
|
||||
let mut child_entries_iter = snapshot.child_entries(path);
|
||||
if let Some(child) = child_entries_iter.next() {
|
||||
if child_entries_iter.next().is_none() && child.is_dir() {
|
||||
self.unfolded_dir_ids.remove(&child.id);
|
||||
path = &*child.path;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_visible_entries(None, cx);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.selection {
|
||||
let (mut worktree_ix, mut entry_ix, _) =
|
||||
@@ -1153,6 +1261,7 @@ impl ProjectPanel {
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
|
||||
let project = self.project.read(cx);
|
||||
self.last_worktree_root_id = project
|
||||
.visible_worktrees(cx)
|
||||
@@ -1194,8 +1303,25 @@ impl ProjectPanel {
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.entries(true);
|
||||
|
||||
while let Some(entry) = entry_iter.entry() {
|
||||
if auto_collapse_dirs
|
||||
&& entry.kind.is_dir()
|
||||
&& !self.unfolded_dir_ids.contains(&entry.id)
|
||||
{
|
||||
if let Some(root_path) = snapshot.root_entry() {
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if entry.path != root_path.path
|
||||
&& child_entries.next().is_none()
|
||||
&& child.kind.is_dir()
|
||||
{
|
||||
entry_iter.advance();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible_worktree_entries.push(entry.clone());
|
||||
if Some(entry.id) == new_entry_parent_id {
|
||||
visible_worktree_entries.push(Entry {
|
||||
@@ -1367,16 +1493,32 @@ impl ProjectPanel {
|
||||
}
|
||||
};
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
let (depth, difference) = ProjectPanel::calculate_depth_and_difference(
|
||||
entry,
|
||||
visible_worktree_entries,
|
||||
);
|
||||
|
||||
let filename = match difference {
|
||||
diff if diff > 1 => entry
|
||||
.path
|
||||
.iter()
|
||||
.skip(entry.path.components().count() - diff)
|
||||
.collect::<PathBuf>()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
_ => entry
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or(root_name)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename,
|
||||
icon,
|
||||
path: entry.path.clone(),
|
||||
depth: entry.path.components().count(),
|
||||
depth,
|
||||
kind: entry.kind,
|
||||
is_ignored: entry.is_ignored,
|
||||
is_expanded,
|
||||
@@ -1420,6 +1562,45 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_depth_and_difference(
|
||||
entry: &Entry,
|
||||
visible_worktree_entries: &Vec<Entry>,
|
||||
) -> (usize, usize) {
|
||||
let visible_worktree_paths: HashSet<Arc<Path>> = visible_worktree_entries
|
||||
.iter()
|
||||
.map(|e| e.path.clone())
|
||||
.collect();
|
||||
|
||||
let (depth, difference) = entry
|
||||
.path
|
||||
.ancestors()
|
||||
.skip(1) // Skip the entry itself
|
||||
.find_map(|ancestor| {
|
||||
if visible_worktree_paths.contains(ancestor) {
|
||||
let parent_entry = visible_worktree_entries
|
||||
.iter()
|
||||
.find(|&e| &*e.path == ancestor)
|
||||
.unwrap();
|
||||
|
||||
let entry_path_components_count = entry.path.components().count();
|
||||
let parent_path_components_count = parent_entry.path.components().count();
|
||||
let difference = entry_path_components_count - parent_path_components_count;
|
||||
let depth = parent_entry
|
||||
.path
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.filter(|ancestor| visible_worktree_paths.contains(*ancestor))
|
||||
.count();
|
||||
Some((depth + 1, difference))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
(depth, difference)
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
@@ -1461,7 +1642,10 @@ impl ProjectPanel {
|
||||
.child(if let Some(icon) = &icon {
|
||||
h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color))
|
||||
} else {
|
||||
h_flex().size(IconSize::default().rems()).invisible()
|
||||
h_flex()
|
||||
.size(IconSize::default().rems())
|
||||
.invisible()
|
||||
.flex_none()
|
||||
})
|
||||
.child(
|
||||
if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
|
||||
@@ -1572,6 +1756,8 @@ impl Render for ProjectPanel {
|
||||
.on_action(cx.listener(Self::copy_path))
|
||||
.on_action(cx.listener(Self::copy_relative_path))
|
||||
.on_action(cx.listener(Self::new_search_in_directory))
|
||||
.on_action(cx.listener(Self::unfold_directory))
|
||||
.on_action(cx.listener(Self::fold_directory))
|
||||
.when(!project.is_read_only(), |el| {
|
||||
el.on_action(cx.listener(Self::new_file))
|
||||
.on_action(cx.listener(Self::new_directory))
|
||||
@@ -1983,6 +2169,125 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
"dir_1": {
|
||||
"nested_dir_1": {
|
||||
"nested_dir_2": {
|
||||
"nested_dir_3": {
|
||||
"file_a.java": "// File contents",
|
||||
"file_b.java": "// File contents",
|
||||
"file_c.java": "// File contents",
|
||||
"nested_dir_4": {
|
||||
"nested_dir_5": {
|
||||
"file_d.java": "// File contents",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/root2",
|
||||
json!({
|
||||
"dir_2": {
|
||||
"file_1.java": "// File contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
cx.update(|cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
auto_fold_dirs: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
|
||||
"v root2",
|
||||
" > dir_2",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(
|
||||
&panel,
|
||||
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected",
|
||||
" > nested_dir_4/nested_dir_5",
|
||||
" file_a.java",
|
||||
" file_b.java",
|
||||
" file_c.java",
|
||||
"v root2",
|
||||
" > dir_2",
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(
|
||||
&panel,
|
||||
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
|
||||
cx,
|
||||
);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
|
||||
" v nested_dir_4/nested_dir_5 <== selected",
|
||||
" file_d.java",
|
||||
" file_a.java",
|
||||
" file_b.java",
|
||||
" file_c.java",
|
||||
"v root2",
|
||||
" > dir_2",
|
||||
]
|
||||
);
|
||||
toggle_expand_dir(&panel, "root2/dir_2", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
|
||||
" v nested_dir_4/nested_dir_5",
|
||||
" file_d.java",
|
||||
" file_a.java",
|
||||
" file_b.java",
|
||||
" file_c.java",
|
||||
"v root2",
|
||||
" v dir_2 <== selected",
|
||||
" file_1.java",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -4,14 +4,14 @@ use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProjectPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
@@ -20,6 +20,7 @@ pub struct ProjectPanelSettings {
|
||||
pub git_status: bool,
|
||||
pub indent_size: f32,
|
||||
pub auto_reveal_entries: bool,
|
||||
pub auto_fold_dirs: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -54,6 +55,11 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub auto_reveal_entries: Option<bool>,
|
||||
/// Whether to fold directories automatically
|
||||
/// when directory has only one directory inside.
|
||||
///
|
||||
/// Default: false
|
||||
pub auto_fold_dirs: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for ProjectPanelSettings {
|
||||
|
||||
@@ -3,18 +3,21 @@ use assistant::{AssistantPanel, InlineAssist};
|
||||
use editor::{Editor, EditorSettings};
|
||||
|
||||
use gpui::{
|
||||
Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
|
||||
Subscription, View, ViewContext, WeakView,
|
||||
anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
|
||||
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
|
||||
};
|
||||
use search::{buffer_search, BufferSearchBar};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
|
||||
use ui::{
|
||||
prelude::*, ButtonSize, ButtonStyle, ContextMenu, IconButton, IconName, IconSize, Tooltip,
|
||||
};
|
||||
use workspace::{
|
||||
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: View<BufferSearchBar>,
|
||||
toggle_settings_menu: Option<View<ContextMenu>>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
@@ -29,6 +32,7 @@ impl QuickActionBar {
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
buffer_search_bar,
|
||||
toggle_settings_menu: None,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
@@ -63,6 +67,17 @@ impl QuickActionBar {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
|
||||
div().absolute().bottom_0().right_0().size_0().child(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for QuickActionBar {
|
||||
@@ -70,22 +85,6 @@ impl Render for QuickActionBar {
|
||||
let Some(editor) = self.active_editor() else {
|
||||
return div().id("empty quick action bar");
|
||||
};
|
||||
let inlay_hints_button = Some(QuickActionBarButton::new(
|
||||
"toggle inlay hints",
|
||||
IconName::InlayHint,
|
||||
editor.read(cx).inlay_hints_enabled(),
|
||||
Box::new(editor::actions::ToggleInlayHints),
|
||||
"Toggle Inlay Hints",
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(&editor::actions::ToggleInlayHints, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
.filter(|_| editor.read(cx).supports_inlay_hints(cx));
|
||||
|
||||
let search_button = Some(QuickActionBarButton::new(
|
||||
"toggle buffer search",
|
||||
@@ -122,14 +121,87 @@ impl Render for QuickActionBar {
|
||||
},
|
||||
);
|
||||
|
||||
let editor_settings_dropdown =
|
||||
IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(self.toggle_settings_menu.is_some())
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
cx.listener(move |quick_action_bar, _, cx| {
|
||||
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
let git_blame_inline_enabled = editor.read(cx).git_blame_inline_enabled();
|
||||
|
||||
let menu = ContextMenu::build(cx, |mut menu, _| {
|
||||
if supports_inlay_hints {
|
||||
menu = menu.toggleable_entry(
|
||||
"Show Inlay Hints",
|
||||
inlay_hints_enabled,
|
||||
Some(editor::actions::ToggleInlayHints.boxed_clone()),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_inlay_hints(
|
||||
&editor::actions::ToggleInlayHints,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.toggleable_entry(
|
||||
"Show Git Blame Inline",
|
||||
git_blame_inline_enabled,
|
||||
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_git_blame_inline(
|
||||
&editor::actions::ToggleGitBlameInline,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
menu
|
||||
});
|
||||
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
|
||||
quick_action_bar.toggle_settings_menu = None;
|
||||
})
|
||||
.detach();
|
||||
quick_action_bar.toggle_settings_menu = Some(menu);
|
||||
})
|
||||
})
|
||||
.when(self.toggle_settings_menu.is_none(), |this| {
|
||||
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id("quick action bar")
|
||||
.gap_2()
|
||||
.children(inlay_hints_button)
|
||||
.children(search_button)
|
||||
.when(AssistantSettings::get_global(cx).button, |bar| {
|
||||
bar.child(assistant_button)
|
||||
})
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.children(search_button)
|
||||
.when(AssistantSettings::get_global(cx).button, |bar| {
|
||||
bar.child(assistant_button)
|
||||
}),
|
||||
)
|
||||
.child(editor_settings_dropdown)
|
||||
.when_some(
|
||||
self.toggle_settings_menu.as_ref(),
|
||||
|el, toggle_settings_menu| {
|
||||
el.child(Self::render_menu_overlay(toggle_settings_menu))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
|
||||
SharedString, StrikethroughStyle, StyledText, UnderlineStyle, WindowContext,
|
||||
AnyElement, AnyView, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
||||
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, WindowContext,
|
||||
};
|
||||
use language::{HighlightId, Language, LanguageRegistry};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
@@ -31,12 +31,16 @@ impl From<HighlightId> for Highlight {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct RichText {
|
||||
pub text: SharedString,
|
||||
pub highlights: Vec<(Range<usize>, Highlight)>,
|
||||
pub link_ranges: Vec<Range<usize>>,
|
||||
pub link_urls: Arc<[String]>,
|
||||
|
||||
pub custom_ranges: Vec<Range<usize>>,
|
||||
custom_ranges_tooltip_fn:
|
||||
Option<Arc<dyn Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView>>>,
|
||||
}
|
||||
|
||||
/// Allows one to specify extra links to the rendered markdown, which can be used
|
||||
@@ -48,7 +52,14 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
|
||||
pub fn set_tooltip_builder_for_custom_ranges(
|
||||
&mut self,
|
||||
f: impl Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView> + 'static,
|
||||
) {
|
||||
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
|
||||
}
|
||||
|
||||
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let code_background = theme.colors().surface_background;
|
||||
|
||||
@@ -111,12 +122,21 @@ impl RichText {
|
||||
.tooltip({
|
||||
let link_ranges = self.link_ranges.clone();
|
||||
let link_urls = self.link_urls.clone();
|
||||
let custom_tooltip_ranges = self.custom_ranges.clone();
|
||||
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
|
||||
move |idx, cx| {
|
||||
for (ix, range) in link_ranges.iter().enumerate() {
|
||||
if range.contains(&idx) {
|
||||
return Some(LinkPreview::new(&link_urls[ix], cx));
|
||||
}
|
||||
}
|
||||
for range in &custom_tooltip_ranges {
|
||||
if range.contains(&idx) {
|
||||
if let Some(f) = &custom_tooltip_fn {
|
||||
return f(idx, range.clone(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
@@ -354,6 +374,8 @@ pub fn render_rich_text(
|
||||
link_urls: link_urls.into(),
|
||||
link_ranges,
|
||||
highlights,
|
||||
custom_ranges: Vec::new(),
|
||||
custom_ranges_tooltip_fn: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ path = "src/rope.rs"
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.1"
|
||||
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
|
||||
log.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
|
||||
@@ -4,7 +4,6 @@ mod point_utf16;
|
||||
mod unclipped;
|
||||
|
||||
use arrayvec::ArrayString;
|
||||
use bromberg_sl2::HashMatrix;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp, fmt, io, mem,
|
||||
@@ -26,12 +25,6 @@ const CHUNK_BASE: usize = 6;
|
||||
#[cfg(not(test))]
|
||||
const CHUNK_BASE: usize = 64;
|
||||
|
||||
/// Type alias to [`HashMatrix`], an implementation of a homomorphic hash function. Two [`Rope`] instances
|
||||
/// containing the same text will produce the same fingerprint. This hash function is special in that
|
||||
/// it allows us to hash individual chunks and aggregate them up the [`Rope`]'s tree, with the resulting
|
||||
/// hash being equivalent to hashing all the text contained in the [`Rope`] at once.
|
||||
pub type RopeFingerprint = HashMatrix;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Rope {
|
||||
chunks: SumTree<Chunk>,
|
||||
@@ -42,10 +35,6 @@ impl Rope {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
|
||||
bromberg_sl2::hash_strict(text.as_bytes())
|
||||
}
|
||||
|
||||
pub fn append(&mut self, rope: Rope) {
|
||||
let mut chunks = rope.chunks.cursor::<()>();
|
||||
chunks.next(&());
|
||||
@@ -424,10 +413,6 @@ impl Rope {
|
||||
self.clip_point(Point::new(row, u32::MAX), Bias::Left)
|
||||
.column
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> RopeFingerprint {
|
||||
self.chunks.summary().fingerprint
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Rope {
|
||||
@@ -994,14 +979,12 @@ impl sum_tree::Item for Chunk {
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ChunkSummary {
|
||||
text: TextSummary,
|
||||
fingerprint: RopeFingerprint,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ChunkSummary {
|
||||
fn from(text: &'a str) -> Self {
|
||||
Self {
|
||||
text: TextSummary::from(text),
|
||||
fingerprint: Rope::text_fingerprint(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,7 +994,6 @@ impl sum_tree::Summary for ChunkSummary {
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.text += &summary.text;
|
||||
self.fingerprint = self.fingerprint * summary.fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,11 @@ message Envelope {
|
||||
LanguageModelResponse language_model_response = 167;
|
||||
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
|
||||
CountTokensResponse count_tokens_response = 169;
|
||||
GetCachedEmbeddings get_cached_embeddings = 189;
|
||||
GetCachedEmbeddingsResponse get_cached_embeddings_response = 190;
|
||||
ComputeEmbeddings compute_embeddings = 191;
|
||||
ComputeEmbeddingsResponse compute_embeddings_response = 192; // current max
|
||||
|
||||
UpdateChannelMessage update_channel_message = 170;
|
||||
ChannelMessageUpdate channel_message_update = 171;
|
||||
|
||||
@@ -216,7 +221,7 @@ message Envelope {
|
||||
MultiLspQueryResponse multi_lsp_query_response = 176;
|
||||
|
||||
CreateRemoteProject create_remote_project = 177;
|
||||
CreateRemoteProjectResponse create_remote_project_response = 188; // current max
|
||||
CreateRemoteProjectResponse create_remote_project_response = 188;
|
||||
CreateDevServer create_dev_server = 178;
|
||||
CreateDevServerResponse create_dev_server_response = 179;
|
||||
ShutdownDevServer shutdown_dev_server = 180;
|
||||
@@ -750,7 +755,7 @@ message BufferSaved {
|
||||
uint64 buffer_id = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
Timestamp mtime = 4;
|
||||
string fingerprint = 5;
|
||||
reserved 5;
|
||||
}
|
||||
|
||||
message BufferReloaded {
|
||||
@@ -758,7 +763,7 @@ message BufferReloaded {
|
||||
uint64 buffer_id = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
Timestamp mtime = 4;
|
||||
string fingerprint = 5;
|
||||
reserved 5;
|
||||
LineEnding line_ending = 6;
|
||||
}
|
||||
|
||||
@@ -1605,7 +1610,7 @@ message BufferState {
|
||||
optional string diff_base = 4;
|
||||
LineEnding line_ending = 5;
|
||||
repeated VectorClockEntry saved_version = 6;
|
||||
string saved_version_fingerprint = 7;
|
||||
reserved 7;
|
||||
Timestamp saved_mtime = 8;
|
||||
}
|
||||
|
||||
@@ -1892,6 +1897,29 @@ message CountTokensResponse {
|
||||
uint32 token_count = 1;
|
||||
}
|
||||
|
||||
message GetCachedEmbeddings {
|
||||
string model = 1;
|
||||
repeated bytes digests = 2;
|
||||
}
|
||||
|
||||
message GetCachedEmbeddingsResponse {
|
||||
repeated Embedding embeddings = 1;
|
||||
}
|
||||
|
||||
message ComputeEmbeddings {
|
||||
string model = 1;
|
||||
repeated string texts = 2;
|
||||
}
|
||||
|
||||
message ComputeEmbeddingsResponse {
|
||||
repeated Embedding embeddings = 1;
|
||||
}
|
||||
|
||||
message Embedding {
|
||||
bytes digest = 1;
|
||||
repeated float dimensions = 2;
|
||||
}
|
||||
|
||||
message BlameBuffer {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
|
||||
@@ -151,6 +151,8 @@ messages!(
|
||||
(ChannelMessageSent, Foreground),
|
||||
(ChannelMessageUpdate, Foreground),
|
||||
(CompleteWithLanguageModel, Background),
|
||||
(ComputeEmbeddings, Background),
|
||||
(ComputeEmbeddingsResponse, Background),
|
||||
(CopyProjectEntry, Foreground),
|
||||
(CountTokensWithLanguageModel, Background),
|
||||
(CountTokensResponse, Background),
|
||||
@@ -174,6 +176,8 @@ messages!(
|
||||
(FormatBuffers, Foreground),
|
||||
(FormatBuffersResponse, Foreground),
|
||||
(FuzzySearchUsers, Foreground),
|
||||
(GetCachedEmbeddings, Background),
|
||||
(GetCachedEmbeddingsResponse, Background),
|
||||
(GetChannelMembers, Foreground),
|
||||
(GetChannelMembersResponse, Foreground),
|
||||
(GetChannelMessages, Background),
|
||||
@@ -325,6 +329,7 @@ request_messages!(
|
||||
(CancelCall, Ack),
|
||||
(CopyProjectEntry, ProjectEntryResponse),
|
||||
(CompleteWithLanguageModel, LanguageModelResponse),
|
||||
(ComputeEmbeddings, ComputeEmbeddingsResponse),
|
||||
(CountTokensWithLanguageModel, CountTokensResponse),
|
||||
(CreateChannel, CreateChannelResponse),
|
||||
(CreateProjectEntry, ProjectEntryResponse),
|
||||
@@ -336,6 +341,7 @@ request_messages!(
|
||||
(Follow, FollowResponse),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
(FuzzySearchUsers, UsersResponse),
|
||||
(GetCachedEmbeddings, GetCachedEmbeddingsResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMessagesById, GetChannelMessagesResponse),
|
||||
|
||||
@@ -272,28 +272,21 @@ impl Render for BufferSearchBar {
|
||||
"Select previous match",
|
||||
&SelectPrevMatch,
|
||||
))
|
||||
.when(!narrow_mode, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.mx(rems_from_px(-4.0))
|
||||
.min_w(rems_from_px(40.))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(match_text).color(
|
||||
if self.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(render_nav_button(
|
||||
ui::IconName::ChevronRight,
|
||||
self.active_match_index.is_some(),
|
||||
"Select next match",
|
||||
&SelectNextMatch,
|
||||
)),
|
||||
))
|
||||
.when(!narrow_mode, |this| {
|
||||
this.child(h_flex().min_w(rems_from_px(40.)).child(
|
||||
Label::new(match_text).color(if self.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
}),
|
||||
))
|
||||
}),
|
||||
);
|
||||
|
||||
let replace_line = should_show_replace_input.then(|| {
|
||||
@@ -531,6 +524,7 @@ impl BufferSearchBar {
|
||||
}
|
||||
}
|
||||
if let Some(active_editor) = self.active_searchable_item.as_ref() {
|
||||
active_editor.search_bar_visibility_changed(false, cx);
|
||||
let handle = active_editor.focus_handle(cx);
|
||||
cx.focus(&handle);
|
||||
}
|
||||
@@ -572,10 +566,12 @@ impl BufferSearchBar {
|
||||
}
|
||||
|
||||
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.active_searchable_item.is_none() {
|
||||
let Some(handle) = self.active_searchable_item.as_ref() else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
self.dismissed = false;
|
||||
handle.search_bar_visibility_changed(true, cx);
|
||||
cx.notify();
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
@@ -1100,6 +1096,7 @@ mod tests {
|
||||
use editor::{DisplayPoint, Editor};
|
||||
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
|
||||
use language::Buffer;
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt as _;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
@@ -1110,6 +1107,7 @@ mod tests {
|
||||
editor::init(cx);
|
||||
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
}
|
||||
@@ -1868,8 +1866,7 @@ mod tests {
|
||||
// Let's turn on regex mode.
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| {
|
||||
search_bar.enable_search_option(SearchOptions::REGEX, cx);
|
||||
search_bar.search("\\[([^\\]]+)\\]", None, cx)
|
||||
search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1892,8 +1889,11 @@ mod tests {
|
||||
// Now with a whole-word twist.
|
||||
search_bar
|
||||
.update(cx, |search_bar, cx| {
|
||||
search_bar.enable_search_option(SearchOptions::REGEX, cx);
|
||||
search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
|
||||
search_bar.search(
|
||||
"a\\w+s",
|
||||
Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -54,8 +54,6 @@ struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
|
||||
|
||||
impl Global for ActiveSettings {}
|
||||
|
||||
const SEARCH_CONTEXT: u32 = 2;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(ActiveSettings::default());
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
@@ -234,7 +232,7 @@ impl ProjectSearch {
|
||||
excerpts.stream_excerpts_with_context_lines(
|
||||
buffer,
|
||||
ranges,
|
||||
SEARCH_CONTEXT,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1437,20 +1435,6 @@ impl Render for ProjectSearchBar {
|
||||
Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.mx(rems_from_px(-4.0))
|
||||
.min_w(rems_from_px(40.))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
Label::new(match_text).color(if search.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("project-search-next-match", IconName::ChevronRight)
|
||||
.disabled(search.active_match_index.is_none())
|
||||
@@ -1463,6 +1447,17 @@ impl Render for ProjectSearchBar {
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w(rems_from_px(40.))
|
||||
.child(
|
||||
Label::new(match_text).color(if search.active_match_index.is_some() {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when(limit_reached, |this| {
|
||||
this.child(
|
||||
div()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user