Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbab23daa | ||
|
|
d742b3bfac | ||
|
|
3bf412feff | ||
|
|
8030e8cf47 | ||
|
|
45429a4528 | ||
|
|
0755ce6486 | ||
|
|
87d3f59515 | ||
|
|
ac74a72a9e | ||
|
|
ae2c23bd8e | ||
|
|
8da6e62914 | ||
|
|
55185c159b | ||
|
|
c9a53b63a7 | ||
|
|
1ab0af2fa3 | ||
|
|
06674a21f9 | ||
|
|
54aecd21ec | ||
|
|
c906fd232d | ||
|
|
d08d4174a5 | ||
|
|
1a82470897 | ||
|
|
583273b6ee | ||
|
|
fcbc220408 | ||
|
|
f09da1a1c8 | ||
|
|
e1efa7298e | ||
|
|
430f5d5d53 | ||
|
|
15edc46827 | ||
|
|
f2ba969d5b | ||
|
|
2d58226a9b | ||
|
|
115f0672fb | ||
|
|
1f6bd6760f | ||
|
|
a0b52cc69a | ||
|
|
5360c0ea28 | ||
|
|
3995c22414 | ||
|
|
659423a4a1 | ||
|
|
074acacdf7 | ||
|
|
6f6cb53fad | ||
|
|
3d76ed96f5 | ||
|
|
cf4f3ed79a | ||
|
|
bacb2a266a | ||
|
|
33e5ba6278 | ||
|
|
11bd28870a | ||
|
|
01ddf840f5 | ||
|
|
ec9f44727e | ||
|
|
998f6cf80d | ||
|
|
980d4f1003 | ||
|
|
79c1003b34 | ||
|
|
d576cda789 | ||
|
|
10cd978e93 | ||
|
|
ce4c15dca6 | ||
|
|
2940a0ebd8 | ||
|
|
8fed9aaec2 | ||
|
|
5ed3b44686 | ||
|
|
69e0ea92e4 | ||
|
|
b35a7223b6 | ||
|
|
020c38a891 | ||
|
|
21f4da6bf2 | ||
|
|
da44f637ed | ||
|
|
0897c8eebd | ||
|
|
7b9d51929d | ||
|
|
5424c8bfd5 | ||
|
|
3521b50405 | ||
|
|
d4264cbe4e | ||
|
|
97be0a930c | ||
|
|
3107ed847a | ||
|
|
944a1f8fb0 | ||
|
|
47a1ff7df9 | ||
|
|
b9d5eb17a3 | ||
|
|
adc7cfb0d3 | ||
|
|
a853a80634 | ||
|
|
2d41a119b3 | ||
|
|
0102ffbfca | ||
|
|
0edffd9248 | ||
|
|
e65a76f0ec | ||
|
|
6c93c4bd35 | ||
|
|
8bafc61ef5 | ||
|
|
3b882918f7 | ||
|
|
5e64d45194 | ||
|
|
3df7da236d | ||
|
|
5e81d780bd | ||
|
|
cbc2746d70 | ||
|
|
aaba98d8ec | ||
|
|
2cc2a61c77 | ||
|
|
3025e5620d | ||
|
|
c4083c3cf6 | ||
|
|
2187513026 | ||
|
|
5b7b5bfea5 |
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -2,4 +2,8 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
|
||||
- Added/Fixed/Improved ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
**or**
|
||||
|
||||
- N/A
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
107
.github/workflows/deploy_collab.yml
vendored
Normal file
107
.github/workflows/deploy_collab.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Publish Collab Server Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- collab-production
|
||||
- collab-staging
|
||||
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check formatting and Clippy lints
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
publish:
|
||||
name: Publish collab server image
|
||||
needs:
|
||||
- style
|
||||
- tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Sign into DigitalOcean docker registry
|
||||
run: doctl registry login
|
||||
|
||||
- name: Prune Docker system
|
||||
run: docker system prune --filter 'until=720h' -f
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
|
||||
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
|
||||
deploy:
|
||||
name: Deploy new server image
|
||||
needs:
|
||||
- publish
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
|
||||
steps:
|
||||
- name: Sign into Kubernetes
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }}
|
||||
|
||||
- name: Determine namespace
|
||||
run: |
|
||||
set -eu
|
||||
if [[ $GITHUB_REF_NAME = "collab-production" ]]; then
|
||||
echo "Deploying collab:$GITHUB_SHA to production"
|
||||
echo "KUBE_NAMESPACE=production" >> $GITHUB_ENV
|
||||
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
|
||||
echo "Deploying collab:$GITHUB_SHA to staging"
|
||||
echo "KUBE_NAMESPACE=staging" >> $GITHUB_ENV
|
||||
else
|
||||
echo "cowardly refusing to deploy from an unknown branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Start rollout
|
||||
run: kubectl -n "$KUBE_NAMESPACE" set image deployment/collab collab=registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
|
||||
- name: Wait for rollout to finish
|
||||
run: kubectl -n "$KUBE_NAMESPACE" rollout status deployment/collab
|
||||
49
.github/workflows/publish_collab_image.yml
vendored
49
.github/workflows/publish_collab_image.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Publish Collab Server Image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- collab-v*
|
||||
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish collab server image
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- deploy
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Sign into DigitalOcean docker registry
|
||||
run: doctl registry login
|
||||
|
||||
- name: Prune Docker system
|
||||
run: docker system prune
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Determine version
|
||||
run: |
|
||||
set -eu
|
||||
version=$(script/get-crate-version collab)
|
||||
if [[ $GITHUB_REF_NAME != "collab-v${version}" ]]; then
|
||||
echo "release tag ${GITHUB_REF_NAME} does not match version ${version}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Publishing collab version: ${version}"
|
||||
echo "COLLAB_VERSION=${version}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
|
||||
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
|
||||
1
.github/workflows/release_nightly.yml
vendored
1
.github/workflows/release_nightly.yml
vendored
@@ -59,6 +59,7 @@ jobs:
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
157
Cargo.lock
generated
157
Cargo.lock
generated
@@ -103,9 +103,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
|
||||
version = "0.20.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=2d2b894c3b869fadc78fce9d72cb5c8d2b764cac#2d2b894c3b869fadc78fce9d72cb5c8d2b764cac"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"bitflags 2.4.1",
|
||||
@@ -441,6 +440,18 @@ dependencies = [
|
||||
"event-listener",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-native-tls"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"native-tls",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "1.7.0"
|
||||
@@ -570,19 +581,6 @@ version = "4.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
||||
|
||||
[[package]]
|
||||
name = "async-tls"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustls 0.19.1",
|
||||
"webpki",
|
||||
"webpki-roots 0.21.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.73"
|
||||
@@ -600,7 +598,8 @@ version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5682ea0913e5c20780fe5785abacb85a411e7437bf52a1bedb93ddb3972cb8dd"
|
||||
dependencies = [
|
||||
"async-tls",
|
||||
"async-native-tls",
|
||||
"async-std",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -1367,6 +1366,7 @@ dependencies = [
|
||||
"image",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
@@ -1377,6 +1377,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2 0.10.7",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"sysinfo",
|
||||
@@ -1438,7 +1439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.43.0"
|
||||
version = "0.44.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1484,6 +1485,7 @@ dependencies = [
|
||||
"prometheus",
|
||||
"prost 0.8.0",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"reqwest",
|
||||
"rpc",
|
||||
"scrypt",
|
||||
@@ -2414,6 +2416,7 @@ dependencies = [
|
||||
"itertools 0.10.5",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
@@ -2422,6 +2425,7 @@ dependencies = [
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -2658,6 +2662,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
@@ -3862,12 +3867,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json_comments"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105"
|
||||
|
||||
[[package]]
|
||||
name = "jwt"
|
||||
version = "0.16.0"
|
||||
@@ -4017,6 +4016,7 @@ dependencies = [
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -4135,6 +4135,15 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
@@ -4261,6 +4270,7 @@ dependencies = [
|
||||
"lsp-types",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -4314,6 +4324,26 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu",
|
||||
"project",
|
||||
"pulldown-cmark",
|
||||
"rich_text",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@@ -5795,6 +5825,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -5859,6 +5890,7 @@ dependencies = [
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
@@ -6032,6 +6064,7 @@ dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"search",
|
||||
"settings",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
@@ -6702,19 +6735,6 @@ dependencies = [
|
||||
"rustix 0.38.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"log",
|
||||
"ring",
|
||||
"sct 0.6.1",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.7"
|
||||
@@ -6723,7 +6743,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct 0.7.0",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6866,16 +6886,6 @@ dependencies = [
|
||||
"sha2 0.9.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
@@ -7597,7 +7607,7 @@ dependencies = [
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls 0.21.7",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7611,7 +7621,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.4.1",
|
||||
"webpki-roots 0.24.0",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8124,6 +8134,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
@@ -8222,7 +8233,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"indexmap 1.9.3",
|
||||
"indoc",
|
||||
"json_comments",
|
||||
"log",
|
||||
"palette",
|
||||
"pathfinder_color",
|
||||
@@ -8230,6 +8240,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"simplelog",
|
||||
"strum",
|
||||
"theme",
|
||||
@@ -8783,6 +8794,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-erlang"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ced5145ebb17f83243bf055b74e108da7cc129e12faab4166df03f59b287f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-gitcommit"
|
||||
version = "0.3.3"
|
||||
@@ -8921,6 +8942,15 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-ocaml"
|
||||
version = "0.20.4"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-ocaml?rev=4abfdc1c7af2c6c77a370aee974627be1c285b3b#4abfdc1c7af2c6c77a370aee974627be1c285b3b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-php"
|
||||
version = "0.21.1"
|
||||
@@ -9090,6 +9120,7 @@ dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha-1 0.9.8",
|
||||
"thiserror",
|
||||
@@ -9419,6 +9450,7 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -9796,25 +9828,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
|
||||
dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.24.0"
|
||||
@@ -10304,6 +10317,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"itertools 0.11.0",
|
||||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
@@ -10312,6 +10326,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"mimalloc",
|
||||
"node_runtime",
|
||||
@@ -10361,6 +10376,7 @@ dependencies = [
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-elm",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-erlang",
|
||||
"tree-sitter-gitcommit",
|
||||
"tree-sitter-gleam",
|
||||
"tree-sitter-glsl",
|
||||
@@ -10375,6 +10391,7 @@ dependencies = [
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-nix",
|
||||
"tree-sitter-nu",
|
||||
"tree-sitter-ocaml",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-proto",
|
||||
"tree-sitter-purescript",
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -42,6 +42,7 @@ members = [
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/multi_buffer",
|
||||
@@ -99,11 +100,14 @@ ctor = "0.2.6"
|
||||
derive_more = "0.99.17"
|
||||
env_logger = "0.9"
|
||||
futures = "0.3"
|
||||
git2 = { version = "0.15", default-features = false}
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
globset = "0.4"
|
||||
indoc = "1"
|
||||
# We explicitly disable a http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"static-curl",
|
||||
"text-decoding",
|
||||
] }
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = "2.1.1"
|
||||
@@ -111,6 +115,7 @@ parking_lot = "0.11.1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.8"
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
@@ -120,7 +125,10 @@ schemars = "0.8"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.1", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
] }
|
||||
serde_repr = "0.1"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
@@ -135,11 +143,12 @@ tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" }
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
|
||||
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-erlang = "0.4.0"
|
||||
tree-sitter-gitcommit = { git = "https://github.com/gbprod/tree-sitter-gitcommit" }
|
||||
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
|
||||
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
|
||||
@@ -154,8 +163,9 @@ tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" }
|
||||
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
|
||||
tree-sitter-php = "0.21.1"
|
||||
tree-sitter-proto = {git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
|
||||
|
||||
@@ -6,6 +6,9 @@ COPY . .
|
||||
|
||||
# Compile collab server
|
||||
ARG CARGO_PROFILE_RELEASE_PANIC=abort
|
||||
ARG GITHUB_SHA
|
||||
|
||||
ENV GITHUB_SHA=$GITHUB_SHA
|
||||
RUN --mount=type=cache,target=./script/node_modules \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=./target \
|
||||
|
||||
1
assets/icons/file_icons/erlang.svg
Normal file
1
assets/icons/file_icons/erlang.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="64" viewBox="0 0 128 128" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="0" x2="128" y1="128" y2="0"><stop offset="0" stop-color="#333"/><stop offset="1" stop-color="#5d5d5d"/></linearGradient><path d="m12.239265 30.664279h14.960911c-5.59432 5.460938-7.654216 10.692785-10.342106 18.023379-3.200764 8.729348-.549141 29.987457 3.815534 37.55289 2.943384 5.101853 6.282685 8.994876 8.233522 11.095173h-16.667861zm89.614855 0h13.90661v66.671442h-13.55518c1.31391-1.750328 3.43934-4.534454 5.12085-6.426163 2.32782-2.618784 4.97023-6.978412 4.97023-6.978412l-16.015202-8.133112s-5.48977 11.600331-15.964999 15.964998c-10.475214 4.364666-19.784679-.838179-25.604243-7.530659-5.819578-6.692502-5.82371-22.14014-5.82371-22.14014h60.797524c1.16391-14.839892-2.63216-21.249816-4.66901-25.90547-.91799-2.098266-1.89261-3.810819-3.16287-5.522484zm-38.356164 1.757154c.35429-.01632.731685-.0092 1.104497 0 11.930114.290977 13.053143 12.802122 13.053143 12.802122h-27.311192s2.170772-12.298638 13.153552-12.802122z" fill="url(#a)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"suffixes": {
|
||||
"Emakefile": "erlang",
|
||||
"aac": "audio",
|
||||
"accdb": "storage",
|
||||
"app.src": "erlang",
|
||||
"avif": "image",
|
||||
"bak": "backup",
|
||||
"bash": "terminal",
|
||||
@@ -23,6 +25,8 @@
|
||||
"doc": "document",
|
||||
"docx": "document",
|
||||
"eex": "elixir",
|
||||
"erl": "erlang",
|
||||
"escript": "erlang",
|
||||
"eslintrc": "eslint",
|
||||
"eslintrc.js": "eslint",
|
||||
"eslintrc.json": "eslint",
|
||||
@@ -37,17 +41,18 @@
|
||||
"gif": "image",
|
||||
"gitattributes": "vcs",
|
||||
"gitignore": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"gitkeep": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"go": "go",
|
||||
"h": "code",
|
||||
"handlebars": "code",
|
||||
"hbs": "template",
|
||||
"heex": "elixir",
|
||||
"heif": "image",
|
||||
"hrl": "erlang",
|
||||
"hs": "haskell",
|
||||
"htm": "template",
|
||||
"html": "template",
|
||||
"hs": "haskell",
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
"ini": "settings",
|
||||
@@ -64,6 +69,8 @@
|
||||
"mdb": "storage",
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
"mp3": "audio",
|
||||
"mp4": "video",
|
||||
"myd": "storage",
|
||||
@@ -85,6 +92,7 @@
|
||||
"psd": "image",
|
||||
"py": "python",
|
||||
"rb": "ruby",
|
||||
"rebar.config": "erlang",
|
||||
"rkt": "code",
|
||||
"rs": "rust",
|
||||
"rtf": "document",
|
||||
@@ -104,13 +112,15 @@
|
||||
"txt": "document",
|
||||
"vue": "vue",
|
||||
"wav": "audio",
|
||||
"webp": "image",
|
||||
"webm": "video",
|
||||
"webp": "image",
|
||||
"xls": "document",
|
||||
"xlsx": "document",
|
||||
"xml": "template",
|
||||
"xrl": "erlang",
|
||||
"yaml": "settings",
|
||||
"yml": "settings",
|
||||
"yrl": "erlang",
|
||||
"zlogin": "terminal",
|
||||
"zsh": "terminal",
|
||||
"zsh_aliases": "terminal",
|
||||
@@ -133,7 +143,7 @@
|
||||
"icon": "icons/file_icons/folder.svg"
|
||||
},
|
||||
"css": {
|
||||
"icon": "icons/file_icons/css.svg"
|
||||
"icon": "icons/file_icons/css.svg"
|
||||
},
|
||||
"default": {
|
||||
"icon": "icons/file_icons/file.svg"
|
||||
@@ -144,6 +154,9 @@
|
||||
"elixir": {
|
||||
"icon": "icons/file_icons/elixir.svg"
|
||||
},
|
||||
"erlang": {
|
||||
"icon": "icons/file_icons/erlang.svg"
|
||||
},
|
||||
"eslint": {
|
||||
"icon": "icons/file_icons/eslint.svg"
|
||||
},
|
||||
@@ -168,6 +181,9 @@
|
||||
"log": {
|
||||
"icon": "icons/file_icons/info.svg"
|
||||
},
|
||||
"ocaml": {
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
},
|
||||
"phoenix": {
|
||||
"icon": "icons/file_icons/phoenix.svg"
|
||||
},
|
||||
|
||||
5
assets/icons/file_icons/ocaml.svg
Normal file
5
assets/icons/file_icons/ocaml.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.73843 11.709C6.70584 11.6334 6.60683 11.4367 6.55712 11.3736C6.44917 11.2362 6.42396 11.2258 6.39221 11.0523C6.33703 10.7501 6.19094 10.202 6.01879 9.82381C5.92987 9.62863 5.78201 9.46467 5.64665 9.32312C5.52847 9.19895 5.26214 8.99002 5.2157 9.00037C4.7807 9.09487 4.64576 9.55902 4.44115 9.92673C4.32795 10.1301 4.208 10.3031 4.1188 10.5195C4.03641 10.7184 4.04373 10.9387 3.90268 11.1095C3.75801 11.2849 3.66398 11.4715 3.59311 11.6981C3.57968 11.7412 3.54148 12.1939 3.5 12.3006L4.14649 12.2511C4.74896 12.2958 4.57496 12.547 5.51526 12.4922L7 12.4423C6.95398 12.2942 6.89056 12.1228 6.86613 12.067C6.82472 11.9732 6.77267 11.7897 6.73843 11.709Z" fill="black"/>
|
||||
<path d="M8.72454 8.42043C8.61775 8.50889 8.40904 8.72165 7.95506 8.8021C7.75133 8.83825 7.56076 8.84122 7.35158 8.82925C7.24918 8.8236 7.15263 8.81758 7.04997 8.81605C6.9895 8.81552 6.78663 8.80812 6.79667 8.83039L6.77408 8.89506C6.7776 8.91633 6.78497 8.96949 6.78703 8.98237C6.79534 9.03461 6.79766 9.07617 6.79939 9.12421C6.80252 9.22297 6.79228 9.32592 6.79667 9.42559C6.80577 9.63232 6.87262 9.82076 6.88106 10.0293C6.89029 10.2615 6.99037 10.5072 7.08718 10.6969C7.12393 10.7691 7.17988 10.7773 7.20426 10.8663C7.23284 10.9681 7.20579 11.0762 7.21968 11.1848C7.27417 11.6058 7.37982 12.0459 7.54501 12.4259C7.54621 12.4291 7.54747 12.4325 7.54893 12.4354C7.75293 12.3961 7.95732 12.3119 8.22239 12.2669C8.70839 12.1841 9.3843 12.2268 9.81848 12.1801C10.9171 12.0616 11.5133 12.6972 12.5 12.4367V3.09052C12.5 2.21217 11.8798 1.5 11.1142 1.5H2.88578C2.1205 1.5 1.5 2.21217 1.5 3.09052V6.5608C1.69828 6.47851 1.98348 5.99435 2.07285 5.87661C2.2292 5.67071 2.25758 5.40808 2.33546 5.24267C2.51281 4.86596 2.54331 4.60691 2.94645 4.60691C3.13436 4.60691 3.20899 4.65663 3.3361 4.85238C3.42454 4.98851 3.57731 5.24 3.64881 5.40815C3.73134 5.60215 3.86583 5.86464 3.92497 5.91763C3.96876 5.95698 4.01221 5.9865 4.05275 6.00396C4.11813 6.0321 4.17222 5.98047 4.21595 5.94051C4.27176 5.8895 4.29582 5.78556 4.34751 5.64692C4.422 5.44689 4.5032 5.20721 4.54938 5.12356C4.62932 4.97897 4.65656 4.80739 4.74288 4.72427C4.8702 4.60172 5.03632 4.59311 5.08203 4.58274C5.33779 4.52478 5.45408 4.72419 5.58006 4.85315C5.66253 4.93764 5.77522 5.10785 5.85523 5.33594C5.91776 5.51408 5.99736 5.67887 6.03065 5.78174C6.06281 5.88103 6.14222 6.04018 6.18926 6.23098C6.23199 6.40424 6.34635 6.537 6.3898 6.61936C6.3898 6.61936 6.45632 6.83319 6.86079 7.02864C6.9485 7.07104 7.12579 7.13998 7.23157 7.18413C7.40733 7.25741 7.57757 7.24788 7.79432 7.21807C7.94888 7.21807 8.03261 6.96123 8.10284 6.75556C8.14437 6.634 8.18418 6.28566 8.21129 6.18675C8.23754 6.09051 8.17614 6.01608 8.22843 5.93174C8.28956 5.83329 8.32591 5.82796 8.3612 5.69961C8.43701 5.42478 8.87524 5.4109 9.12156 5.4109C9.32689 5.4109 9.30078 5.63967 9.6491 5.56143C9.84858 5.51652 10.0408 5.59094 10.2526 5.65531C10.4309 5.7096 10.5986 5.77145 10.699 5.90642C10.764 5.99382 10.9254 6.43169 10.761 6.45037C10.7768 6.47257 10.7884 6.5126 10.8179 6.53456C10.7813 6.69974 10.622 6.58207 10.5335 6.56087C10.4142 6.5325 10.33 6.56514 10.2133 6.6244C10.0138 6.72635 9.72206 6.71446 9.5483 6.88055C9.40085 7.02132 9.40111 7.33558 9.33234 7.51166C9.33234 7.51166 9.14137 8.07551 8.72454 8.42043Z" fill="black"/>
|
||||
<path d="M3.6514 8.80413C3.57333 8.79137 3.50083 8.77702 3.425 8.75238C3.28339 8.70644 3.12867 8.66165 2.9892 8.60788C2.90451 8.57488 2.62239 8.41409 2.5611 8.36877C2.41737 8.26211 2.32191 7.97231 2.20955 8.00214C2.13782 8.02098 2.06795 8.06058 2.02333 8.17701C1.98692 8.27196 1.97457 8.43521 1.94936 8.54469C1.92011 8.67177 1.86959 8.7904 1.82536 8.91149C1.74401 9.13362 1.59759 9.33453 1.5345 9.55093C1.52181 9.59546 1.51055 9.64527 1.5 9.6972V10.5274V11.9637V12.1713C1.57359 12.1915 1.65057 12.2164 1.73674 12.2535C2.37264 12.5265 2.52781 12.5497 3.15152 12.4348L3.21002 12.4223C3.25775 12.2625 3.2946 11.718 3.32555 11.5494C3.34966 11.4202 3.38279 11.3173 3.39537 11.1853C3.40723 11.06 3.39427 10.9406 3.3876 10.8267C3.37011 10.5414 3.51669 10.4395 3.58667 10.1945C3.64976 9.97283 3.68617 9.7206 3.73839 9.49399C3.78847 9.27653 3.86665 8.96922 4 8.85975C3.98382 8.82938 3.72144 8.81539 3.6514 8.80413Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -42,6 +42,7 @@
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-alt-l": "editor::Format",
|
||||
"shift-f6": "editor::Rename",
|
||||
"cmd-[": "pane::GoBack",
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
@@ -83,7 +84,8 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open"
|
||||
"enter": "project_panel::Open",
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
";": "vim::RepeatFind",
|
||||
",": "vim::RepeatFindReversed",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-i": "pane::GoForward",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
@@ -333,8 +335,6 @@
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
";": "vim::RepeatFind",
|
||||
",": "vim::RepeatFindReversed",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
@@ -502,5 +502,18 @@
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -104,8 +104,17 @@
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone muted by default
|
||||
"mute_on_join": false
|
||||
// Join calls with the microphone live by default
|
||||
"mute_on_join": false,
|
||||
// Share your project when you are the first to join a channel
|
||||
"share_on_join": true
|
||||
},
|
||||
// Toolbar related settings
|
||||
"toolbar": {
|
||||
// Whether to show breadcrumbs.
|
||||
"breadcrumbs": true,
|
||||
// Whether to show quick action buttons.
|
||||
"quick_actions": true
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -127,8 +136,12 @@
|
||||
// Whether to show selections in the scrollbar.
|
||||
"selections": true,
|
||||
// Whether to show symbols selections in the scrollbar.
|
||||
"symbols_selections": true
|
||||
"symbols_selections": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
"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:
|
||||
@@ -496,6 +509,12 @@
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml Interface": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
|
||||
{}
|
||||
|
||||
@@ -199,9 +199,13 @@ impl AssistantPanel {
|
||||
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
|
||||
cx.notify();
|
||||
if self.focus_handle.is_focused(cx) {
|
||||
if let Some(editor) = self.active_editor() {
|
||||
cx.focus_view(editor);
|
||||
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
if self.has_credentials() {
|
||||
if let Some(editor) = self.active_editor() {
|
||||
cx.focus_view(editor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
cx.focus_view(api_key_editor);
|
||||
}
|
||||
}
|
||||
@@ -777,6 +781,10 @@ impl AssistantPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
|
||||
self.api_key_editor = Some(build_api_key_editor(cx));
|
||||
}
|
||||
|
||||
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
|
||||
let editor = cx.new_view(|cx| {
|
||||
ConversationEditor::new(
|
||||
@@ -870,7 +878,7 @@ impl AssistantPanel {
|
||||
cx.update(|cx| completion_provider.delete_credentials(cx))?
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_key_editor = Some(build_api_key_editor(cx));
|
||||
this.build_api_key_editor(cx);
|
||||
this.focus_handle.focus(cx);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -1136,7 +1144,7 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
|
||||
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
|
||||
@@ -1147,9 +1155,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if let Some(api_key_editor) = self.api_key_editor.clone() {
|
||||
const INSTRUCTIONS: [&'static str; 5] = [
|
||||
const INSTRUCTIONS: [&'static str; 6] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
|
||||
" - You can create an API key at: platform.openai.com/api-keys",
|
||||
" - Make sure your OpenAI account has credits",
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
" ",
|
||||
"Paste your OpenAI API key and press Enter to use the assistant:"
|
||||
@@ -1342,7 +1351,9 @@ impl Panel for AssistantPanel {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_credentials.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.editors.is_empty() {
|
||||
if !this.has_credentials() {
|
||||
this.build_api_key_editor(cx);
|
||||
} else if this.editors.is_empty() {
|
||||
this.new_conversation(cx);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
@@ -108,29 +108,28 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
updater
|
||||
});
|
||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||
}
|
||||
updater
|
||||
});
|
||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
|
||||
@@ -40,10 +40,11 @@ impl Render for UpdateNotification {
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(|_, cx| {
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
crate::view_release_notes(&Default::default(), cx);
|
||||
}),
|
||||
)
|
||||
this.dismiss(&menu::Cancel, cx)
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use settings::Settings;
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
pub mute_on_join: bool,
|
||||
pub share_on_join: bool,
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
@@ -16,6 +17,11 @@ pub struct CallSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
pub mute_on_join: Option<bool>,
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: true
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
|
||||
@@ -617,6 +617,10 @@ impl Room {
|
||||
self.local_participant.role == proto::ChannelRole::Admin
|
||||
}
|
||||
|
||||
pub fn local_participant_is_guest(&self) -> bool {
|
||||
self.local_participant.role == proto::ChannelRole::Guest
|
||||
}
|
||||
|
||||
pub fn set_participant_role(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
@@ -1202,7 +1206,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn share_project(
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
|
||||
@@ -74,11 +74,19 @@ impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
+ "channel/"
|
||||
+ &self.slug()
|
||||
+ &Self::slug(&self.name)
|
||||
+ "-"
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn notes_link(&self, heading: Option<String>) -> String {
|
||||
self.link()
|
||||
+ "/notes"
|
||||
+ &heading
|
||||
.map(|h| format!("#{}", Self::slug(&h)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_root_channel(&self) -> bool {
|
||||
self.parent_path.is_empty()
|
||||
}
|
||||
@@ -90,9 +98,8 @@ impl Channel {
|
||||
.unwrap_or(self.id)
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
pub fn slug(str: &str) -> String {
|
||||
let slug: String = str
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect();
|
||||
|
||||
@@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
|
||||
@@ -27,11 +27,12 @@ sum_tree = { path = "../sum_tree" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-tls"] }
|
||||
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
once_cell = "1.19.0"
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
@@ -39,6 +40,7 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2 = "0.10"
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -15,14 +15,13 @@ use futures::{
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion,
|
||||
Task, WeakModel,
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use release_channel::ReleaseChannel;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -58,9 +57,6 @@ lazy_static! {
|
||||
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
pub static ref ZED_APP_VERSION: Option<SemanticVersion> = std::env::var("ZED_APP_VERSION")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok());
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||
@@ -1011,13 +1007,22 @@ impl Client {
|
||||
.update(|cx| ReleaseChannel::try_global(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
let app_version = cx
|
||||
.update(|cx| AppVersion::global(cx).to_string())
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let request = Request::builder()
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("{} {}", credentials.user_id, credentials.access_token),
|
||||
)
|
||||
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION);
|
||||
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
|
||||
.header("x-zed-app-version", app_version)
|
||||
.header(
|
||||
"x-zed-release-channel",
|
||||
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -1035,7 +1040,7 @@ impl Client {
|
||||
rpc_url.set_scheme("wss").unwrap();
|
||||
let request = request.uri(rpc_url.as_str()).body(())?;
|
||||
let (stream, _) =
|
||||
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
|
||||
async_tungstenite::async_std::client_async_tls(request, stream).await?;
|
||||
Ok(Connection::new(
|
||||
stream
|
||||
.map_err(|error| anyhow!(error))
|
||||
|
||||
@@ -4,16 +4,19 @@ use crate::TelemetrySettings;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::ReleaseChannel;
|
||||
use serde::Serialize;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Write;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{
|
||||
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::{HttpClient, ZedHttpClient};
|
||||
use util::http::{self, HttpClient, Method, ZedHttpClient};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use util::ResultExt;
|
||||
use util::TryFutureExt;
|
||||
@@ -142,6 +145,16 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
|
||||
#[cfg(not(debug_assertions))]
|
||||
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
|
||||
|
||||
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
option_env!("ZED_CLIENT_CHECKSUM_SEED")
|
||||
.map(|s| s.as_bytes().into())
|
||||
.or_else(|| {
|
||||
env::var("ZED_CLIENT_CHECKSUM_SEED")
|
||||
.ok()
|
||||
.map(|s| s.as_bytes().into())
|
||||
})
|
||||
});
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
let release_channel =
|
||||
@@ -500,6 +513,10 @@ impl Telemetry {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
|
||||
return;
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
@@ -540,9 +557,27 @@ impl Telemetry {
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
.post_json(&this.http_client.zed_url("/api/events"), json_bytes.into())
|
||||
.await?;
|
||||
let mut summer = Sha256::new();
|
||||
summer.update(checksum_seed);
|
||||
summer.update(&json_bytes);
|
||||
summer.update(checksum_seed);
|
||||
let mut checksum = String::new();
|
||||
for byte in summer.finalize().as_slice() {
|
||||
use std::fmt::Write;
|
||||
write!(&mut checksum, "{:02x}", byte).unwrap();
|
||||
}
|
||||
|
||||
let request = http::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(&this.http_client.zed_url("/api/events"))
|
||||
.header("Content-Type", "text/plain")
|
||||
.header("x-zed-checksum", checksum)
|
||||
.body(json_bytes.into());
|
||||
|
||||
let response = this.http_client.send(request?).await?;
|
||||
if response.status() != 200 {
|
||||
log::error!("Failed to send events: HTTP {:?}", response.status());
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.43.0"
|
||||
version = "0.44.0"
|
||||
publish = false
|
||||
license = "AGPL-3.0-or-later"
|
||||
|
||||
@@ -61,6 +61,7 @@ util = { path = "../util" }
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
release_channel = { path = "../release_channel" }
|
||||
async-trait.workspace = true
|
||||
audio = { path = "../audio" }
|
||||
call = { path = "../call", features = ["test-support"] }
|
||||
|
||||
@@ -3,3 +3,35 @@
|
||||
This crate is what we run at https://collab.zed.dev.
|
||||
|
||||
It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel.
|
||||
|
||||
# Local Development
|
||||
|
||||
Detailed instructions on getting started are [here](https://zed.dev/docs/local-collaboration).
|
||||
|
||||
# Deployment
|
||||
|
||||
We run two instances of collab:
|
||||
|
||||
* Staging (https://staging-collab.zed.dev)
|
||||
* Production (https://collab.zed.dev)
|
||||
|
||||
Both of these run on the Kubernetes cluster hosted in Digital Ocean.
|
||||
|
||||
Deployment is triggered by pushing to the `collab-staging` (or `collab-production`) tag in Github. The best way to do this is:
|
||||
|
||||
* `./script/deploy-collab staging`
|
||||
* `./script/deploy-collab production`
|
||||
|
||||
You can tell what is currently deployed with `./script/what-is-deployed`.
|
||||
|
||||
# Database Migrations
|
||||
|
||||
To create a new migration:
|
||||
|
||||
```
|
||||
./script/sqlx migrate add <name>
|
||||
```
|
||||
|
||||
Migrations are run automatically on service start, so run `foreman start` again. The service will crash if the migrations fail.
|
||||
|
||||
When you create a new migration, you also need to update the [SQLite schema](./migrations.sqlite/20221109000000_test_schema.sql) that is used for testing.
|
||||
|
||||
@@ -14,6 +14,7 @@ use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
|
||||
use util::ResultExt;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -26,7 +27,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
match args().skip(1).next().as_deref() {
|
||||
Some("version") => {
|
||||
println!("collab v{VERSION}");
|
||||
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
|
||||
}
|
||||
Some("migrate") => {
|
||||
run_migrations().await?;
|
||||
@@ -105,7 +106,7 @@ async fn run_migrations() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn handle_root() -> String {
|
||||
format!("collab v{VERSION}")
|
||||
format!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"))
|
||||
}
|
||||
|
||||
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
|
||||
|
||||
@@ -64,6 +64,7 @@ use time::OffsetDateTime;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{field, info_span, instrument, Instrument};
|
||||
use util::SemanticVersion;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -795,6 +796,7 @@ fn broadcast<F>(
|
||||
|
||||
lazy_static! {
|
||||
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version");
|
||||
static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version");
|
||||
}
|
||||
|
||||
pub struct ProtocolVersion(u32);
|
||||
@@ -824,6 +826,32 @@ impl Header for ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppVersionHeader(SemanticVersion);
|
||||
impl Header for AppVersionHeader {
|
||||
fn name() -> &'static HeaderName {
|
||||
&ZED_APP_VERSION
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Iterator<Item = &'i axum::http::HeaderValue>,
|
||||
{
|
||||
let version = values
|
||||
.next()
|
||||
.ok_or_else(axum::headers::Error::invalid)?
|
||||
.to_str()
|
||||
.map_err(|_| axum::headers::Error::invalid())?
|
||||
.parse()
|
||||
.map_err(|_| axum::headers::Error::invalid())?;
|
||||
Ok(Self(version))
|
||||
}
|
||||
|
||||
fn encode<E: Extend<axum::http::HeaderValue>>(&self, values: &mut E) {
|
||||
values.extend([self.0.to_string().parse().unwrap()]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(server: Arc<Server>) -> Router<Body> {
|
||||
Router::new()
|
||||
.route("/rpc", get(handle_websocket_request))
|
||||
@@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
|
||||
|
||||
pub async fn handle_websocket_request(
|
||||
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
|
||||
_app_version_header: Option<TypedHeader<AppVersionHeader>>,
|
||||
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
||||
Extension(server): Extension<Arc<Server>>,
|
||||
Extension(user): Extension<User>,
|
||||
@@ -851,6 +880,7 @@ pub async fn handle_websocket_request(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
ws.on_upgrade(move |socket| {
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -161,15 +161,15 @@ async fn test_channel_notes_participant_indices(
|
||||
|
||||
// Clients A, B, and C open the channel notes
|
||||
let channel_view_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_c = cx_c
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -644,7 +644,7 @@ async fn test_channel_buffer_changes(
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
@@ -1905,7 +1905,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 1.
|
||||
let channel_notes_1_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_1_a.update(cx_a, |notes, cx| {
|
||||
@@ -1951,7 +1951,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
|
||||
// Client A opens the notes for channel 2.
|
||||
let channel_notes_2_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
|
||||
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_notes_2_a.update(cx_a, |notes, cx| {
|
||||
|
||||
@@ -153,6 +153,7 @@ impl TestServer {
|
||||
}
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ use client::{
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint, scroll::Autoscroll, CollaborationHub, DisplayPoint, Editor,
|
||||
EditorEvent,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
actions, AnyElement, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter,
|
||||
FocusableView, IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
@@ -23,10 +26,10 @@ use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(collab, [Deploy]);
|
||||
actions!(collab, [CopyLink]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
@@ -34,21 +37,30 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
_reparse_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
link_position: Option<String>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
let channel_view = Self::open_in_pane(
|
||||
channel_id,
|
||||
link_position,
|
||||
pane.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
@@ -66,10 +78,12 @@ impl ChannelView {
|
||||
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
link_position: Option<String>,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
@@ -82,12 +96,13 @@ impl ChannelView {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
channel_buffer.update(&mut cx, |channel_buffer, cx| {
|
||||
channel_buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
let Some(markdown) = markdown else {
|
||||
return;
|
||||
};
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
})
|
||||
})?;
|
||||
|
||||
@@ -101,12 +116,18 @@ impl ChannelView {
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
if let Some(link_position) = link_position {
|
||||
existing_view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
let mut this =
|
||||
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
@@ -121,6 +142,12 @@ impl ChannelView {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(link_position) = link_position {
|
||||
view.update(cx, |channel_view, cx| {
|
||||
channel_view.focus_position_from_link(link_position, true, cx)
|
||||
});
|
||||
}
|
||||
|
||||
view
|
||||
})
|
||||
})
|
||||
@@ -128,16 +155,29 @@ impl ChannelView {
|
||||
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
workspace: WeakView<Workspace>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let this = cx.view().downgrade();
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_custom_context_menu(move |_, position, cx| {
|
||||
let this = this.clone();
|
||||
Some(ui::ContextMenu::build(cx, move |menu, _| {
|
||||
menu.entry("Copy link to section", None, move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.copy_link_for_position(position.clone(), cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}))
|
||||
});
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
@@ -148,14 +188,94 @@ impl ChannelView {
|
||||
|
||||
Self {
|
||||
editor,
|
||||
workspace,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
_reparse_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_position_from_link(
|
||||
&mut self,
|
||||
position: String,
|
||||
first_attempt: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let position = Channel::slug(&position).to_lowercase();
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
|
||||
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
|
||||
if let Some(item) = outline
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
|
||||
{
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
|
||||
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(&map)])
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !first_attempt {
|
||||
return;
|
||||
}
|
||||
self._reparse_subscription = Some(cx.subscribe(
|
||||
&self.editor,
|
||||
move |this, _, e: &EditorEvent, cx| {
|
||||
match e {
|
||||
EditorEvent::Reparsed => {
|
||||
this.focus_position_from_link(position.clone(), false, cx);
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
|
||||
this._reparse_subscription.take();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
|
||||
let position = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
|
||||
self.copy_link_for_position(position, cx)
|
||||
}
|
||||
|
||||
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
|
||||
let mut closest_heading = None;
|
||||
|
||||
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
|
||||
for item in outline.items {
|
||||
if item.range.start.to_display_point(&snapshot) > position {
|
||||
break;
|
||||
}
|
||||
closest_heading = Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(channel) = self.channel(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let link = channel.notes_link(closest_heading.map(|heading| heading.text));
|
||||
cx.write_to_clipboard(ClipboardItem::new(link));
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(Toast::new(0, "Link copied to clipboard"), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
@@ -215,8 +335,11 @@ impl ChannelView {
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
impl Render for ChannelView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.editor.clone()
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::copy_link))
|
||||
.child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +397,7 @@ impl Item for ChannelView {
|
||||
Some(cx.new_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.workspace.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
@@ -356,7 +480,7 @@ impl FollowableItem for ChannelView {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
let open = ChannelView::open_in_pane(state.channel_id, None, pane, workspace, cx);
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
@@ -453,7 +453,7 @@ impl ChatPanel {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
|
||||
rich_text::render_rich_text(message.body.clone(), &mentions, language_registry, None)
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
|
||||
Workspace,
|
||||
OpenChannelNotes, Workspace,
|
||||
};
|
||||
|
||||
actions!(
|
||||
@@ -69,6 +69,19 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<CollabPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|_, _: &OpenChannelNotes, cx| {
|
||||
let channel_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.and_then(|room| room.read(cx).channel_id());
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
let workspace = cx.view().clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -957,7 +970,7 @@ impl CollabPanel {
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, IconName::File)),
|
||||
)
|
||||
.child(div().h_7().w_full().child(Label::new("notes")))
|
||||
.child(Label::new("notes"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
}
|
||||
|
||||
@@ -1678,7 +1691,7 @@ impl CollabPanel {
|
||||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
ChannelView::open(channel_id, None, workspace, cx).detach();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||
WindowKind, WindowOptions,
|
||||
WindowContext, WindowKind, WindowOptions,
|
||||
};
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::AppState;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
@@ -41,7 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
notifications::init(&app_state, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut WindowContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
@@ -64,7 +64,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", cx, |e, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ impl Copilot {
|
||||
)
|
||||
.detach();
|
||||
|
||||
let server = server.initialize(Default::default()).await?;
|
||||
let server = cx.update(|cx| server.initialize(None, cx))?.await?;
|
||||
|
||||
let status = server
|
||||
.request::<request::CheckStatus>(request::CheckStatusParams {
|
||||
|
||||
@@ -20,7 +20,7 @@ test-support = [
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript"
|
||||
"tree-sitter-typescript",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -33,13 +33,14 @@ convert_case = "0.6.0"
|
||||
copilot = { path = "../copilot" }
|
||||
db = { path = "../db" }
|
||||
futures.workspace = true
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
indoc = "1.0.4"
|
||||
itertools = "0.10"
|
||||
language = { path = "../language" }
|
||||
lazy_static.workspace = true
|
||||
linkify = "0.10.0"
|
||||
log.workspace = true
|
||||
lsp = { path = "../lsp" }
|
||||
multi_buffer = { path = "../multi_buffer" }
|
||||
@@ -79,6 +80,7 @@ language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
release_channel = { path = "../release_channel" }
|
||||
rand.workspace = true
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
|
||||
@@ -70,6 +70,30 @@ pub struct FoldAt {
|
||||
pub struct UnfoldAt {
|
||||
pub buffer_row: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct MoveUpByLines {
|
||||
#[serde(default)]
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct MoveDownByLines {
|
||||
#[serde(default)]
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct SelectUpByLines {
|
||||
#[serde(default)]
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct SelectDownByLines {
|
||||
#[serde(default)]
|
||||
pub(super) lines: u32,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -84,7 +108,11 @@ impl_actions!(
|
||||
ConfirmCodeAction,
|
||||
ToggleComments,
|
||||
FoldAt,
|
||||
UnfoldAt
|
||||
UnfoldAt,
|
||||
MoveUpByLines,
|
||||
MoveDownByLines,
|
||||
SelectUpByLines,
|
||||
SelectDownByLines,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ mod wrap_map;
|
||||
|
||||
use crate::EditorStyle;
|
||||
use crate::{
|
||||
link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
|
||||
InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
hover_links::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, InlayId,
|
||||
MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
@@ -586,8 +586,9 @@ impl DisplaySnapshot {
|
||||
text_system,
|
||||
editor_style,
|
||||
rem_size,
|
||||
anchor: _,
|
||||
scroll_anchor: _,
|
||||
visible_rows: _,
|
||||
vertical_scroll_margin: _,
|
||||
}: &TextLayoutDetails,
|
||||
) -> Arc<LineLayout> {
|
||||
let mut runs = Vec::new();
|
||||
|
||||
@@ -1168,7 +1168,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{InlayHighlights, TextHighlights},
|
||||
link_go_to_definition::InlayHighlight,
|
||||
hover_links::InlayHighlight,
|
||||
InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::AppContext;
|
||||
|
||||
@@ -22,9 +22,9 @@ mod inlay_hint_cache;
|
||||
mod debounced_delay;
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
mod hover_popover;
|
||||
pub mod items;
|
||||
mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
@@ -74,10 +74,10 @@ use language::{
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
||||
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
|
||||
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
|
||||
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use movement::TextLayoutDetails;
|
||||
@@ -374,6 +374,7 @@ pub struct Editor {
|
||||
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
|
||||
pub show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
show_breadcrumbs: bool,
|
||||
show_gutter: bool,
|
||||
show_wrap_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
@@ -402,7 +403,7 @@ pub struct Editor {
|
||||
remote_id: Option<ViewId>,
|
||||
hover_state: HoverState,
|
||||
gutter_hovered: bool,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
hovered_link_state: Option<HoveredLinkState>,
|
||||
copilot_state: CopilotState,
|
||||
inlay_hint_cache: InlayHintCache,
|
||||
next_inlay_id: usize,
|
||||
@@ -413,6 +414,12 @@ pub struct Editor {
|
||||
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
show_copilot_suggestions: bool,
|
||||
use_autoclose: bool,
|
||||
custom_context_menu: Option<
|
||||
Box<
|
||||
dyn 'static
|
||||
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
pub struct EditorSnapshot {
|
||||
@@ -845,15 +852,21 @@ impl CompletionsMenu {
|
||||
let selected_item = self.selected_item;
|
||||
let style = style.clone();
|
||||
|
||||
let multiline_docs = {
|
||||
let multiline_docs = if show_completion_documentation {
|
||||
let mat = &self.matches[selected_item];
|
||||
let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation {
|
||||
Some(Documentation::MultiLinePlainText(text)) => {
|
||||
Some(div().child(SharedString::from(text.clone())))
|
||||
}
|
||||
Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child(
|
||||
render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx),
|
||||
)),
|
||||
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
||||
Some(div().child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
multiline_docs.map(|div| {
|
||||
@@ -870,6 +883,8 @@ impl CompletionsMenu {
|
||||
// because that would move the cursor.
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let list = uniform_list(
|
||||
@@ -1418,7 +1433,7 @@ impl Editor {
|
||||
buffer: buffer.clone(),
|
||||
display_map: display_map.clone(),
|
||||
selections,
|
||||
scroll_manager: ScrollManager::new(),
|
||||
scroll_manager: ScrollManager::new(cx),
|
||||
columnar_selection_tail: None,
|
||||
add_selections_state: None,
|
||||
select_next_state: None,
|
||||
@@ -1436,6 +1451,7 @@ impl Editor {
|
||||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
mode,
|
||||
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
show_wrap_guides: None,
|
||||
placeholder_text: None,
|
||||
@@ -1465,7 +1481,7 @@ impl Editor {
|
||||
leader_peer_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
link_go_to_definition_state: Default::default(),
|
||||
hovered_link_state: Default::default(),
|
||||
copilot_state: Default::default(),
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
gutter_hovered: false,
|
||||
@@ -1476,6 +1492,7 @@ impl Editor {
|
||||
hovered_cursors: Default::default(),
|
||||
editor_actions: Default::default(),
|
||||
show_copilot_suggestions: mode == EditorMode::Full,
|
||||
custom_context_menu: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1665,6 +1682,14 @@ impl Editor {
|
||||
self.collaboration_hub = Some(hub);
|
||||
}
|
||||
|
||||
pub fn set_custom_context_menu(
|
||||
&mut self,
|
||||
f: impl 'static
|
||||
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
|
||||
) {
|
||||
self.custom_context_menu = Some(Box::new(f))
|
||||
}
|
||||
|
||||
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
|
||||
self.completion_provider = Some(hub);
|
||||
}
|
||||
@@ -3065,8 +3090,9 @@ impl Editor {
|
||||
text_system: cx.text_system().clone(),
|
||||
editor_style: self.style.clone().unwrap(),
|
||||
rem_size: cx.rem_size(),
|
||||
anchor: self.scroll_manager.anchor().anchor,
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
visible_rows: self.visible_line_count(),
|
||||
vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5358,6 +5384,86 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::up_by_rows(
|
||||
map,
|
||||
selection.start,
|
||||
action.lines,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_down_by_lines(&mut self, action: &MoveDownByLines, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::down_by_rows(
|
||||
map,
|
||||
selection.start,
|
||||
action.lines,
|
||||
selection.goal,
|
||||
false,
|
||||
&text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_down_by_lines(&mut self, action: &SelectDownByLines, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| {
|
||||
movement::down_by_rows(map, head, action.lines, goal, false, &text_layout_details)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_up_by_lines(&mut self, action: &SelectUpByLines, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| {
|
||||
movement::up_by_rows(map, head, action.lines, goal, false, &text_layout_details)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
@@ -7141,11 +7247,8 @@ impl Editor {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.navigate_to_definitions(
|
||||
definitions
|
||||
.into_iter()
|
||||
.map(GoToDefinitionLink::Text)
|
||||
.collect(),
|
||||
editor.navigate_to_hover_links(
|
||||
definitions.into_iter().map(HoverLink::Text).collect(),
|
||||
split,
|
||||
cx,
|
||||
);
|
||||
@@ -7155,9 +7258,9 @@ impl Editor {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn navigate_to_definitions(
|
||||
pub fn navigate_to_hover_links(
|
||||
&mut self,
|
||||
mut definitions: Vec<GoToDefinitionLink>,
|
||||
mut definitions: Vec<HoverLink>,
|
||||
split: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
@@ -7169,10 +7272,14 @@ impl Editor {
|
||||
if definitions.len() == 1 {
|
||||
let definition = definitions.pop().unwrap();
|
||||
let target_task = match definition {
|
||||
GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
|
||||
GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
|
||||
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
|
||||
HoverLink::InlayHint(lsp_location, server_id) => {
|
||||
self.compute_target_location(lsp_location, server_id, cx)
|
||||
}
|
||||
HoverLink::Url(url) => {
|
||||
cx.open_url(&url);
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
};
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let target = target_task.await.context("target resolution task")?;
|
||||
@@ -7223,29 +7330,27 @@ impl Editor {
|
||||
let title = definitions
|
||||
.iter()
|
||||
.find_map(|definition| match definition {
|
||||
GoToDefinitionLink::Text(link) => {
|
||||
link.origin.as_ref().map(|origin| {
|
||||
let buffer = origin.buffer.read(cx);
|
||||
format!(
|
||||
"Definitions for {}",
|
||||
buffer
|
||||
.text_for_range(origin.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
}
|
||||
GoToDefinitionLink::InlayHint(_, _) => None,
|
||||
HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
|
||||
let buffer = origin.buffer.read(cx);
|
||||
format!(
|
||||
"Definitions for {}",
|
||||
buffer
|
||||
.text_for_range(origin.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
}),
|
||||
HoverLink::InlayHint(_, _) => None,
|
||||
HoverLink::Url(_) => None,
|
||||
})
|
||||
.unwrap_or("Definitions".to_string());
|
||||
let location_tasks = definitions
|
||||
.into_iter()
|
||||
.map(|definition| match definition {
|
||||
GoToDefinitionLink::Text(link) => {
|
||||
Task::Ready(Some(Ok(Some(link.target))))
|
||||
}
|
||||
GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
|
||||
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
|
||||
HoverLink::InlayHint(lsp_location, server_id) => {
|
||||
editor.compute_target_location(lsp_location, server_id, cx)
|
||||
}
|
||||
HoverLink::Url(_) => Task::ready(Ok(None)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(title, location_tasks)
|
||||
@@ -7289,9 +7394,7 @@ impl Editor {
|
||||
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
|
||||
project
|
||||
.language_server_for_buffer(buffer.read(cx), server_id, cx)
|
||||
.map(|(_, lsp_adapter)| {
|
||||
LanguageServerName(Arc::from(lsp_adapter.name()))
|
||||
})
|
||||
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
|
||||
});
|
||||
language_server_name.map(|language_server_name| {
|
||||
project.open_local_buffer_via_lsp(
|
||||
@@ -8663,6 +8766,9 @@ impl Editor {
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
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;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ pub struct EditorSettings {
|
||||
pub show_completion_documentation: bool,
|
||||
pub completion_documentation_secondary_query_debounce: u64,
|
||||
pub use_on_type_format: bool,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
pub redact_private_values: bool,
|
||||
@@ -28,12 +30,19 @@ pub enum SeedQuerySetting {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Toolbar {
|
||||
pub breadcrumbs: bool,
|
||||
pub quick_actions: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Scrollbar {
|
||||
pub show: ShowScrollbar,
|
||||
pub git_diff: bool,
|
||||
pub selections: bool,
|
||||
pub symbols_selections: bool,
|
||||
pub diagnostics: bool,
|
||||
}
|
||||
|
||||
/// When to show the scrollbar in the editor.
|
||||
@@ -84,8 +93,15 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub use_on_type_format: Option<bool>,
|
||||
/// Toolbar related settings
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
|
||||
/// The number of lines to keep above/below the cursor when auto-scrolling.
|
||||
///
|
||||
/// Default: 3.
|
||||
pub vertical_scroll_margin: Option<f32>,
|
||||
/// Whether the line numbers on editors gutter are relative or not.
|
||||
///
|
||||
/// Default: false
|
||||
@@ -103,6 +119,19 @@ pub struct EditorSettingsContent {
|
||||
pub redact_private_values: Option<bool>,
|
||||
}
|
||||
|
||||
// Toolbar related settings
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ToolbarContent {
|
||||
/// Whether to display breadcrumbs in the editor toolbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub breadcrumbs: Option<bool>,
|
||||
/// Whether to display quik action buttons in the editor toolbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub quick_actions: Option<bool>,
|
||||
}
|
||||
|
||||
/// Scrollbar related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarContent {
|
||||
@@ -122,6 +151,10 @@ pub struct ScrollbarContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub symbols_selections: Option<bool>,
|
||||
/// Whether to show diagnostic indicators in the scrollbar.
|
||||
///
|
||||
/// Default: true
|
||||
pub diagnostics: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for EditorSettings {
|
||||
|
||||
@@ -8392,6 +8392,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -9,11 +9,6 @@ use crate::{
|
||||
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
},
|
||||
items::BufferSearchHighlights,
|
||||
link_go_to_definition::{
|
||||
go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
|
||||
update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
|
||||
LinkGoToDefinitionState,
|
||||
},
|
||||
mouse_context_menu,
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
@@ -35,6 +30,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::Anchor;
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
@@ -146,7 +142,11 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::move_left);
|
||||
register_action(view, cx, Editor::move_right);
|
||||
register_action(view, cx, Editor::move_down);
|
||||
register_action(view, cx, Editor::move_down_by_lines);
|
||||
register_action(view, cx, Editor::select_down_by_lines);
|
||||
register_action(view, cx, Editor::move_up);
|
||||
register_action(view, cx, Editor::move_up_by_lines);
|
||||
register_action(view, cx, Editor::select_up_by_lines);
|
||||
register_action(view, cx, Editor::cancel);
|
||||
register_action(view, cx, Editor::newline);
|
||||
register_action(view, cx, Editor::newline_above);
|
||||
@@ -332,7 +332,14 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::display_cursor_names);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut ElementContext) {
|
||||
fn register_key_listeners(
|
||||
&self,
|
||||
cx: &mut ElementContext,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
layout: &LayoutState,
|
||||
) {
|
||||
let position_map = layout.position_map.clone();
|
||||
let stacking_order = cx.stacking_order().clone();
|
||||
cx.on_key_event({
|
||||
let editor = self.editor.clone();
|
||||
move |event: &ModifiersChangedEvent, phase, cx| {
|
||||
@@ -340,46 +347,41 @@ impl EditorElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
Self::modifiers_changed(
|
||||
editor,
|
||||
event,
|
||||
&position_map,
|
||||
text_bounds,
|
||||
&stacking_order,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn modifiers_changed(
|
||||
fn modifiers_changed(
|
||||
editor: &mut Editor,
|
||||
event: &ModifiersChangedEvent,
|
||||
position_map: &PositionMap,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
stacking_order: &StackingOrder,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let pending_selection = editor.has_pending_selection();
|
||||
|
||||
if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
|
||||
if event.command && !pending_selection {
|
||||
let point = point.clone();
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let kind = point.definition_kind(event.shift);
|
||||
|
||||
show_link_definition(kind, editor, point, snapshot, cx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
) {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if !text_bounds.contains(&mouse_position)
|
||||
|| !cx.was_top_layer(&mouse_position, stacking_order)
|
||||
{
|
||||
if editor.link_go_to_definition_state.symbol_range.is_some()
|
||||
|| !editor.link_go_to_definition_state.definitions.is_empty()
|
||||
{
|
||||
editor.link_go_to_definition_state.symbol_range.take();
|
||||
editor.link_go_to_definition_state.definitions.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
editor.link_go_to_definition_state.task = None;
|
||||
|
||||
editor.clear_highlights::<LinkGoToDefinitionState>(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
false
|
||||
editor.update_hovered_link(
|
||||
position_map.point_for_position(text_bounds, mouse_position),
|
||||
&position_map.snapshot,
|
||||
event.modifiers,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_left_down(
|
||||
@@ -480,13 +482,7 @@ impl EditorElement {
|
||||
&& cx.was_top_layer(&event.position, stacking_order)
|
||||
{
|
||||
let point = position_map.point_for_position(text_bounds, event.position);
|
||||
let could_be_inlay = point.as_valid().is_none();
|
||||
let split = event.modifiers.alt;
|
||||
if event.modifiers.shift || could_be_inlay {
|
||||
go_to_fetched_type_definition(editor, point, split, cx);
|
||||
} else {
|
||||
go_to_fetched_definition(editor, point, split, cx);
|
||||
}
|
||||
editor.handle_click_hovered_link(point, event.modifiers, cx);
|
||||
|
||||
cx.stop_propagation();
|
||||
} else if end_selection {
|
||||
@@ -559,31 +555,14 @@ impl EditorElement {
|
||||
if text_hovered && was_top {
|
||||
let point_for_position = position_map.point_for_position(text_bounds, event.position);
|
||||
|
||||
match point_for_position.as_valid() {
|
||||
Some(point) => {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
Some(GoToDefinitionTrigger::Text(point)),
|
||||
modifiers.command,
|
||||
modifiers.shift,
|
||||
cx,
|
||||
);
|
||||
hover_at(editor, Some(point), cx);
|
||||
Self::update_visible_cursor(editor, point, position_map, cx);
|
||||
}
|
||||
None => {
|
||||
update_inlay_link_and_hover_points(
|
||||
&position_map.snapshot,
|
||||
point_for_position,
|
||||
editor,
|
||||
modifiers.command,
|
||||
modifiers.shift,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
|
||||
|
||||
if let Some(point) = point_for_position.as_valid() {
|
||||
hover_at(editor, Some(point), cx);
|
||||
Self::update_visible_cursor(editor, point, position_map, cx);
|
||||
}
|
||||
} else {
|
||||
update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
|
||||
editor.hide_hovered_link(cx);
|
||||
hover_at(editor, None, cx);
|
||||
if gutter_hovered && was_top {
|
||||
cx.stop_propagation();
|
||||
@@ -925,13 +904,13 @@ impl EditorElement {
|
||||
if self
|
||||
.editor
|
||||
.read(cx)
|
||||
.link_go_to_definition_state
|
||||
.definitions
|
||||
.is_empty()
|
||||
.hovered_link_state
|
||||
.as_ref()
|
||||
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
|
||||
{
|
||||
cx.set_cursor_style(CursorStyle::IBeam);
|
||||
} else {
|
||||
cx.set_cursor_style(CursorStyle::PointingHand);
|
||||
} else {
|
||||
cx.set_cursor_style(CursorStyle::IBeam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1477,6 +1456,64 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.diagnostics {
|
||||
let max_point = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.max_point();
|
||||
|
||||
let diagnostics = layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.diagnostics_in_range::<_, Point>(Point::zero()..max_point, false)
|
||||
// We want to sort by severity, in order to paint the most severe diagnostics last.
|
||||
.sorted_by_key(|diagnostic| std::cmp::Reverse(diagnostic.diagnostic.severity));
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
let start_display = diagnostic
|
||||
.range
|
||||
.start
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = diagnostic
|
||||
.range
|
||||
.end
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if diagnostic.range.start == diagnostic.range.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
let color = match diagnostic.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => cx.theme().status().error,
|
||||
DiagnosticSeverity::WARNING => cx.theme().status().warning,
|
||||
DiagnosticSeverity::INFORMATION => cx.theme().status().info,
|
||||
_ => cx.theme().status().hint,
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
cx.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
@@ -2106,6 +2143,9 @@ impl EditorElement {
|
||||
// Symbols Selections
|
||||
(is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
|
||||
||
|
||||
// Diagnostics
|
||||
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
|
||||
||
|
||||
// Scrollmanager
|
||||
editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
@@ -3039,9 +3079,9 @@ impl Element for EditorElement {
|
||||
let key_context = self.editor.read(cx).key_context(cx);
|
||||
cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
|
||||
self.register_actions(cx);
|
||||
self.register_key_listeners(cx);
|
||||
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
self.register_key_listeners(cx, text_bounds, &layout);
|
||||
cx.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||||
@@ -3158,16 +3198,6 @@ pub struct PointForPosition {
|
||||
}
|
||||
|
||||
impl PointForPosition {
|
||||
#[cfg(test)]
|
||||
pub fn valid(valid: DisplayPoint) -> Self {
|
||||
Self {
|
||||
previous_valid: valid,
|
||||
next_valid: valid,
|
||||
exact_unclipped: valid,
|
||||
column_overshoot_after_line_end: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_valid(&self) -> Option<DisplayPoint> {
|
||||
if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
|
||||
Some(self.previous_valid)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
link_go_to_definition::{InlayHighlight, RangeInEditor},
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
ExcerptId, Hover, RangeToAnchorExt,
|
||||
};
|
||||
@@ -605,8 +605,8 @@ mod tests {
|
||||
use crate::{
|
||||
editor_tests::init_test,
|
||||
element::PointForPosition,
|
||||
hover_links::update_inlay_link_and_hover_points,
|
||||
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
|
||||
link_go_to_definition::update_inlay_link_and_hover_points,
|
||||
test::editor_lsp_test_context::EditorLspTestContext,
|
||||
InlayId,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
use anyhow::Context;
|
||||
use clock::Global;
|
||||
use futures::future;
|
||||
use gpui::{Model, ModelContext, Task, ViewContext};
|
||||
use gpui::{AsyncWindowContext, Model, ModelContext, Task, ViewContext};
|
||||
use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
|
||||
use parking_lot::RwLock;
|
||||
use project::{InlayHint, ResolveState};
|
||||
@@ -29,7 +29,7 @@ use language::language_settings::InlayHintSettings;
|
||||
use smol::lock::Semaphore;
|
||||
use sum_tree::Bias;
|
||||
use text::{BufferId, ToOffset, ToPoint};
|
||||
use util::post_inc;
|
||||
use util::{post_inc, ResultExt};
|
||||
|
||||
pub struct InlayHintCache {
|
||||
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
|
||||
@@ -78,6 +78,7 @@ pub(super) enum InvalidationStrategy {
|
||||
/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
|
||||
/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
|
||||
/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct InlaySplice {
|
||||
pub to_remove: Vec<InlayId>,
|
||||
pub to_insert: Vec<Inlay>,
|
||||
@@ -619,7 +620,6 @@ fn spawn_new_update_tasks(
|
||||
update_cache_version: usize,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) {
|
||||
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
|
||||
for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
|
||||
excerpts_to_query
|
||||
{
|
||||
@@ -636,8 +636,7 @@ fn spawn_new_update_tasks(
|
||||
continue;
|
||||
}
|
||||
|
||||
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
|
||||
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
|
||||
if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
|
||||
let cached_excerpt_hints = cached_excerpt_hints.read();
|
||||
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
|
||||
if cached_excerpt_hints.version > update_cache_version
|
||||
@@ -647,20 +646,15 @@ fn spawn_new_update_tasks(
|
||||
}
|
||||
};
|
||||
|
||||
let (multi_buffer_snapshot, Some(query_ranges)) =
|
||||
editor.buffer.update(cx, |multi_buffer, cx| {
|
||||
(
|
||||
multi_buffer.snapshot(cx),
|
||||
determine_query_ranges(
|
||||
multi_buffer,
|
||||
excerpt_id,
|
||||
&excerpt_buffer,
|
||||
excerpt_visible_range,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
})
|
||||
else {
|
||||
let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
|
||||
determine_query_ranges(
|
||||
multi_buffer,
|
||||
excerpt_id,
|
||||
&excerpt_buffer,
|
||||
excerpt_visible_range,
|
||||
cx,
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let query = ExcerptQuery {
|
||||
@@ -671,18 +665,8 @@ fn spawn_new_update_tasks(
|
||||
reason,
|
||||
};
|
||||
|
||||
let new_update_task = |query_ranges| {
|
||||
new_update_task(
|
||||
query,
|
||||
query_ranges,
|
||||
multi_buffer_snapshot,
|
||||
buffer_snapshot.clone(),
|
||||
Arc::clone(&visible_hints),
|
||||
cached_excerpt_hints,
|
||||
Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
|
||||
cx,
|
||||
)
|
||||
};
|
||||
let mut new_update_task =
|
||||
|query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
|
||||
|
||||
match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
|
||||
hash_map::Entry::Occupied(mut o) => {
|
||||
@@ -790,62 +774,55 @@ const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
|
||||
fn new_update_task(
|
||||
query: ExcerptQuery,
|
||||
query_ranges: QueryRanges,
|
||||
multi_buffer_snapshot: MultiBufferSnapshot,
|
||||
buffer_snapshot: BufferSnapshot,
|
||||
visible_hints: Arc<Vec<Inlay>>,
|
||||
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
|
||||
lsp_request_limiter: Arc<Semaphore>,
|
||||
excerpt_buffer: Model<Buffer>,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Task<()> {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let closure_cx = cx.clone();
|
||||
let fetch_and_update_hints = |invalidate, range| {
|
||||
fetch_and_update_hints(
|
||||
editor.clone(),
|
||||
multi_buffer_snapshot.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
Arc::clone(&visible_hints),
|
||||
cached_excerpt_hints.as_ref().map(Arc::clone),
|
||||
query,
|
||||
invalidate,
|
||||
range,
|
||||
Arc::clone(&lsp_request_limiter),
|
||||
closure_cx.clone(),
|
||||
)
|
||||
};
|
||||
let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
|
||||
|visible_range| async move {
|
||||
(
|
||||
visible_range.clone(),
|
||||
fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
|
||||
.await,
|
||||
)
|
||||
},
|
||||
))
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
let visible_range_update_results = future::join_all(
|
||||
query_ranges
|
||||
.visible
|
||||
.into_iter()
|
||||
.filter_map(|visible_range| {
|
||||
let fetch_task = editor
|
||||
.update(&mut cx, |_, cx| {
|
||||
fetch_and_update_hints(
|
||||
excerpt_buffer.clone(),
|
||||
query,
|
||||
visible_range.clone(),
|
||||
query.invalidate.should_invalidate(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()?;
|
||||
Some(async move { (visible_range, fetch_task.await) })
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let hint_delay = cx.background_executor().timer(Duration::from_millis(
|
||||
INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
|
||||
));
|
||||
|
||||
let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
|
||||
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
|
||||
editor
|
||||
.update(&mut cx, |editor, _| {
|
||||
if let Some(task_ranges) = editor
|
||||
.inlay_hint_cache
|
||||
.update_tasks
|
||||
.get_mut(&query.excerpt_id)
|
||||
{
|
||||
task_ranges.invalidate_range(&buffer_snapshot, &range);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
};
|
||||
let query_range_failed =
|
||||
|range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncWindowContext| {
|
||||
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
if let Some(task_ranges) = editor
|
||||
.inlay_hint_cache
|
||||
.update_tasks
|
||||
.get_mut(&query.excerpt_id)
|
||||
{
|
||||
let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
|
||||
task_ranges.invalidate_range(&buffer_snapshot, &range);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
};
|
||||
|
||||
for (range, result) in visible_range_update_results {
|
||||
if let Err(e) = result {
|
||||
query_range_failed(&range, e);
|
||||
query_range_failed(&range, e, &mut cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,149 +832,171 @@ fn new_update_task(
|
||||
.before_visible
|
||||
.into_iter()
|
||||
.chain(query_ranges.after_visible.into_iter())
|
||||
.map(|invisible_range| async move {
|
||||
(
|
||||
invisible_range.clone(),
|
||||
fetch_and_update_hints(false, invisible_range).await,
|
||||
)
|
||||
.filter_map(|invisible_range| {
|
||||
let fetch_task = editor
|
||||
.update(&mut cx, |_, cx| {
|
||||
fetch_and_update_hints(
|
||||
excerpt_buffer.clone(),
|
||||
query,
|
||||
invisible_range.clone(),
|
||||
false, // visible screen request already invalidated the entries
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()?;
|
||||
Some(async move { (invisible_range, fetch_task.await) })
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
for (range, result) in invisible_range_update_results {
|
||||
if let Err(e) = result {
|
||||
query_range_failed(&range, e);
|
||||
query_range_failed(&range, e, &mut cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_and_update_hints(
|
||||
editor: gpui::WeakView<Editor>,
|
||||
multi_buffer_snapshot: MultiBufferSnapshot,
|
||||
buffer_snapshot: BufferSnapshot,
|
||||
visible_hints: Arc<Vec<Inlay>>,
|
||||
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
|
||||
fn fetch_and_update_hints(
|
||||
excerpt_buffer: Model<Buffer>,
|
||||
query: ExcerptQuery,
|
||||
invalidate: bool,
|
||||
fetch_range: Range<language::Anchor>,
|
||||
lsp_request_limiter: Arc<Semaphore>,
|
||||
mut cx: gpui::AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
|
||||
(None, false)
|
||||
} else {
|
||||
match lsp_request_limiter.try_acquire() {
|
||||
Some(guard) => (Some(guard), false),
|
||||
None => (Some(lsp_request_limiter.acquire().await), true),
|
||||
}
|
||||
};
|
||||
let fetch_range_to_log =
|
||||
fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
|
||||
let inlay_hints_fetch_task = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
if got_throttled {
|
||||
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
|
||||
Some((_, _, current_visible_range)) => {
|
||||
let visible_offset_length = current_visible_range.len();
|
||||
let double_visible_range = current_visible_range
|
||||
.start
|
||||
.saturating_sub(visible_offset_length)
|
||||
..current_visible_range
|
||||
.end
|
||||
.saturating_add(visible_offset_length)
|
||||
.min(buffer_snapshot.len());
|
||||
!double_visible_range
|
||||
.contains(&fetch_range.start.to_offset(&buffer_snapshot))
|
||||
&& !double_visible_range
|
||||
.contains(&fetch_range.end.to_offset(&buffer_snapshot))
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
if query_not_around_visible_range {
|
||||
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
|
||||
if let Some(task_ranges) = editor
|
||||
.inlay_hint_cache
|
||||
.update_tasks
|
||||
.get_mut(&query.excerpt_id)
|
||||
{
|
||||
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.buffer(query.buffer_id)
|
||||
.and_then(|buffer| {
|
||||
let project = editor.project.as_ref()?;
|
||||
Some(project.update(cx, |project, cx| {
|
||||
project.inlay_hints(buffer, fetch_range.clone(), cx)
|
||||
}))
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let new_hints = match inlay_hints_fetch_task {
|
||||
Some(fetch_task) => {
|
||||
log::debug!(
|
||||
"Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
|
||||
query_reason = query.reason,
|
||||
);
|
||||
log::trace!(
|
||||
"Currently visible hints: {visible_hints:?}, cached hints present: {}",
|
||||
cached_excerpt_hints.is_some(),
|
||||
);
|
||||
fetch_task.await.context("inlay hint fetch task")?
|
||||
}
|
||||
None => return Ok(()),
|
||||
};
|
||||
drop(lsp_request_guard);
|
||||
log::debug!(
|
||||
"Fetched {} hints for range {fetch_range_to_log:?}",
|
||||
new_hints.len()
|
||||
);
|
||||
log::trace!("Fetched hints: {new_hints:?}");
|
||||
invalidate: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let buffer_snapshot = excerpt_buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
let (lsp_request_limiter, multi_buffer_snapshot) = editor.update(&mut cx, |editor, cx| {
|
||||
let multi_buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
|
||||
(lsp_request_limiter, multi_buffer_snapshot)
|
||||
})?;
|
||||
|
||||
let background_task_buffer_snapshot = buffer_snapshot.clone();
|
||||
let background_fetch_range = fetch_range.clone();
|
||||
let new_update = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
calculate_hint_updates(
|
||||
query.excerpt_id,
|
||||
invalidate,
|
||||
background_fetch_range,
|
||||
new_hints,
|
||||
&background_task_buffer_snapshot,
|
||||
cached_excerpt_hints,
|
||||
&visible_hints,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
if let Some(new_update) = new_update {
|
||||
log::debug!(
|
||||
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
|
||||
new_update.remove_from_visible.len(),
|
||||
new_update.remove_from_cache.len(),
|
||||
new_update.add_to_cache.len()
|
||||
);
|
||||
log::trace!("New update: {new_update:?}");
|
||||
editor
|
||||
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
|
||||
(None, false)
|
||||
} else {
|
||||
match lsp_request_limiter.try_acquire() {
|
||||
Some(guard) => (Some(guard), false),
|
||||
None => (Some(lsp_request_limiter.acquire().await), true),
|
||||
}
|
||||
};
|
||||
let fetch_range_to_log =
|
||||
fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
|
||||
let inlay_hints_fetch_task = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
apply_hint_update(
|
||||
editor,
|
||||
new_update,
|
||||
query,
|
||||
invalidate,
|
||||
buffer_snapshot,
|
||||
multi_buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
if got_throttled {
|
||||
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
|
||||
Some((_, _, current_visible_range)) => {
|
||||
let visible_offset_length = current_visible_range.len();
|
||||
let double_visible_range = current_visible_range
|
||||
.start
|
||||
.saturating_sub(visible_offset_length)
|
||||
..current_visible_range
|
||||
.end
|
||||
.saturating_add(visible_offset_length)
|
||||
.min(buffer_snapshot.len());
|
||||
!double_visible_range
|
||||
.contains(&fetch_range.start.to_offset(&buffer_snapshot))
|
||||
&& !double_visible_range
|
||||
.contains(&fetch_range.end.to_offset(&buffer_snapshot))
|
||||
},
|
||||
None => true,
|
||||
};
|
||||
if query_not_around_visible_range {
|
||||
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
|
||||
if let Some(task_ranges) = editor
|
||||
.inlay_hint_cache
|
||||
.update_tasks
|
||||
.get_mut(&query.excerpt_id)
|
||||
{
|
||||
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.buffer(query.buffer_id)
|
||||
.and_then(|buffer| {
|
||||
let project = editor.project.as_ref()?;
|
||||
Some(project.update(cx, |project, cx| {
|
||||
project.inlay_hints(buffer, fetch_range.clone(), cx)
|
||||
}))
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let cached_excerpt_hints = editor.update(&mut cx, |editor, _| {
|
||||
editor
|
||||
.inlay_hint_cache
|
||||
.hints
|
||||
.get(&query.excerpt_id)
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
let visible_hints = editor.update(&mut cx, |editor, cx| editor.visible_inlay_hints(cx))?;
|
||||
let new_hints = match inlay_hints_fetch_task {
|
||||
Some(fetch_task) => {
|
||||
log::debug!(
|
||||
"Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
|
||||
query_reason = query.reason,
|
||||
);
|
||||
log::trace!(
|
||||
"Currently visible hints: {visible_hints:?}, cached hints present: {}",
|
||||
cached_excerpt_hints.is_some(),
|
||||
);
|
||||
fetch_task.await.context("inlay hint fetch task")?
|
||||
}
|
||||
None => return Ok(()),
|
||||
};
|
||||
drop(lsp_request_guard);
|
||||
log::debug!(
|
||||
"Fetched {} hints for range {fetch_range_to_log:?}",
|
||||
new_hints.len()
|
||||
);
|
||||
log::trace!("Fetched hints: {new_hints:?}");
|
||||
|
||||
let background_task_buffer_snapshot = buffer_snapshot.clone();
|
||||
let background_fetch_range = fetch_range.clone();
|
||||
let new_update = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
calculate_hint_updates(
|
||||
query.excerpt_id,
|
||||
invalidate,
|
||||
background_fetch_range,
|
||||
new_hints,
|
||||
&background_task_buffer_snapshot,
|
||||
cached_excerpt_hints,
|
||||
&visible_hints,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
if let Some(new_update) = new_update {
|
||||
log::debug!(
|
||||
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
|
||||
new_update.remove_from_visible.len(),
|
||||
new_update.remove_from_cache.len(),
|
||||
new_update.add_to_cache.len()
|
||||
);
|
||||
log::trace!("New update: {new_update:?}");
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
apply_hint_update(
|
||||
editor,
|
||||
new_update,
|
||||
query,
|
||||
invalidate,
|
||||
buffer_snapshot,
|
||||
multi_buffer_snapshot,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_hint_updates(
|
||||
@@ -2436,7 +2435,7 @@ pub mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
@@ -3216,6 +3215,7 @@ pub mod tests {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
|
||||
persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings,
|
||||
ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
|
||||
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
|
||||
NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashSet;
|
||||
@@ -682,8 +682,7 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
hide_link_definition(self, cx);
|
||||
self.link_go_to_definition_state.last_trigger_point = None;
|
||||
self.hide_hovered_link(cx);
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
@@ -801,7 +800,11 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
if self.show_breadcrumbs {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
|
||||
@@ -25,31 +25,40 @@ pub fn deploy_context_menu(
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
return;
|
||||
}
|
||||
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
||||
let menu = custom(editor, point, cx);
|
||||
editor.custom_context_menu = Some(custom);
|
||||
if menu.is_none() {
|
||||
return;
|
||||
}
|
||||
menu.unwrap()
|
||||
} else {
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
if editor.project.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
// Move the cursor to the clicked location so that dispatched actions make sense
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
|
||||
let context_menu = ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Find All References", Box::new(FindAllReferences))
|
||||
.action(
|
||||
"Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
});
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Find All References", Box::new(FindAllReferences))
|
||||
.action(
|
||||
"Code Actions",
|
||||
Box::new(ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
})
|
||||
};
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
cx.focus(&context_menu_focus);
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
|
||||
|
||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
|
||||
use gpui::{px, Pixels, TextSystem};
|
||||
use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, ToPoint};
|
||||
use gpui::{px, Pixels, WindowTextSystem};
|
||||
use language::Point;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use multi_buffer::Anchor;
|
||||
|
||||
/// Defines search strategy for items in `movement` module.
|
||||
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
|
||||
/// `FindRange::MultiLine` keeps going until the end of a string.
|
||||
@@ -22,11 +20,12 @@ pub enum FindRange {
|
||||
/// TextLayoutDetails encompasses everything we need to move vertically
|
||||
/// taking into account variable width characters.
|
||||
pub struct TextLayoutDetails {
|
||||
pub(crate) text_system: Arc<TextSystem>,
|
||||
pub(crate) text_system: Arc<WindowTextSystem>,
|
||||
pub(crate) editor_style: EditorStyle,
|
||||
pub(crate) rem_size: Pixels,
|
||||
pub anchor: Anchor,
|
||||
pub scroll_anchor: ScrollAnchor,
|
||||
pub visible_rows: Option<f32>,
|
||||
pub vertical_scroll_margin: f32,
|
||||
}
|
||||
|
||||
/// Returns a column to the left of the current point, wrapping
|
||||
|
||||
@@ -6,13 +6,14 @@ use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
|
||||
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, InlayHintRefreshReason,
|
||||
MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
|
||||
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext};
|
||||
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
|
||||
use language::{Bias, Point};
|
||||
pub use scroll_amount::ScrollAmount;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
time::{Duration, Instant},
|
||||
@@ -21,7 +22,6 @@ use util::ResultExt;
|
||||
use workspace::{ItemId, WorkspaceId};
|
||||
|
||||
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
|
||||
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -128,7 +128,7 @@ impl OngoingScroll {
|
||||
}
|
||||
|
||||
pub struct ScrollManager {
|
||||
vertical_scroll_margin: f32,
|
||||
pub(crate) vertical_scroll_margin: f32,
|
||||
anchor: ScrollAnchor,
|
||||
ongoing: OngoingScroll,
|
||||
autoscroll_request: Option<(Autoscroll, bool)>,
|
||||
@@ -140,9 +140,9 @@ pub struct ScrollManager {
|
||||
}
|
||||
|
||||
impl ScrollManager {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
ScrollManager {
|
||||
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
|
||||
vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
|
||||
anchor: ScrollAnchor::new(),
|
||||
ongoing: OngoingScroll::new(),
|
||||
autoscroll_request: None,
|
||||
|
||||
@@ -12,17 +12,26 @@ pub enum Autoscroll {
|
||||
}
|
||||
|
||||
impl Autoscroll {
|
||||
/// scrolls the minimal amount to (try) and fit all cursors onscreen
|
||||
pub fn fit() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Fit)
|
||||
}
|
||||
|
||||
/// scrolls the minimal amount to fit the newest cursor
|
||||
pub fn newest() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Newest)
|
||||
}
|
||||
|
||||
/// scrolls so the newest cursor is vertically centered
|
||||
pub fn center() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Center)
|
||||
}
|
||||
|
||||
/// scrolls so the neweset cursor is near the top
|
||||
/// (offset by vertical_scroll_margin)
|
||||
pub fn focused() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Focused)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Default, Clone, Copy)]
|
||||
@@ -31,6 +40,7 @@ pub enum AutoscrollStrategy {
|
||||
Newest,
|
||||
#[default]
|
||||
Center,
|
||||
Focused,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
@@ -155,6 +165,11 @@ impl Editor {
|
||||
scroll_position.y = (target_top - margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Focused => {
|
||||
scroll_position.y =
|
||||
(target_top - self.scroll_manager.vertical_scroll_margin).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.y = (target_top).max(0.0);
|
||||
self.set_scroll_position_internal(scroll_position, local, true, cx);
|
||||
|
||||
@@ -4,7 +4,8 @@ use crate::{
|
||||
use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
|
||||
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
|
||||
VisualTestContext,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
@@ -187,6 +188,31 @@ impl EditorTestContext {
|
||||
ranges[0].start.to_display_point(&snapshot)
|
||||
}
|
||||
|
||||
pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
|
||||
let display_point = self.display_point(marked_text);
|
||||
self.pixel_position_for(display_point)
|
||||
}
|
||||
|
||||
pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
|
||||
self.update_editor(|editor, cx| {
|
||||
let newest_point = editor.selections.newest_display(cx).head();
|
||||
let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
|
||||
let line_height = editor
|
||||
.style()
|
||||
.unwrap()
|
||||
.text
|
||||
.line_height_in_pixels(cx.rem_size());
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let details = editor.text_layout_details(cx);
|
||||
|
||||
let y = pixel_position.y
|
||||
+ line_height * (display_point.row() as f32 - newest_point.row() as f32);
|
||||
let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
|
||||
- snapshot.x_for_display_point(newest_point, &details);
|
||||
Point::new(x, y)
|
||||
})
|
||||
}
|
||||
|
||||
// Returns anchors for the current buffer using `«` and `»`
|
||||
pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
|
||||
let ranges = self.ranges(marked_text);
|
||||
@@ -343,7 +369,7 @@ impl EditorTestContext {
|
||||
}
|
||||
|
||||
impl Deref for EditorTestContext {
|
||||
type Target = gpui::TestAppContext;
|
||||
type Target = gpui::VisualTestContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use client::ZED_APP_VERSION;
|
||||
use gpui::AppContext;
|
||||
use human_bytes::human_bytes;
|
||||
use release_channel::ReleaseChannel;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use serde::Serialize;
|
||||
use std::{env, fmt::Display};
|
||||
use sysinfo::{RefreshKind, System, SystemExt};
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SystemSpecs {
|
||||
app_version: Option<String>,
|
||||
app_version: String,
|
||||
release_channel: &'static str,
|
||||
os_name: &'static str,
|
||||
os_version: Option<String>,
|
||||
@@ -18,9 +17,7 @@ pub struct SystemSpecs {
|
||||
|
||||
impl SystemSpecs {
|
||||
pub fn new(cx: &AppContext) -> Self {
|
||||
let app_version = ZED_APP_VERSION
|
||||
.or_else(|| cx.app_metadata().app_version)
|
||||
.map(|v| v.to_string());
|
||||
let app_version = AppVersion::global(cx).to_string();
|
||||
let release_channel = ReleaseChannel::global(cx).display_name();
|
||||
let os_name = cx.app_metadata().os_name;
|
||||
let system = System::new_with_specifics(RefreshKind::new().with_memory());
|
||||
@@ -48,18 +45,15 @@ impl Display for SystemSpecs {
|
||||
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
|
||||
None => format!("OS: {}", self.os_name),
|
||||
};
|
||||
let app_version_information = self
|
||||
.app_version
|
||||
.as_ref()
|
||||
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
|
||||
let app_version_information =
|
||||
format!("Zed: v{} ({})", self.app_version, self.release_channel);
|
||||
let system_specs = [
|
||||
app_version_information,
|
||||
Some(os_information),
|
||||
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
|
||||
Some(format!("Architecture: {}", self.architecture)),
|
||||
os_information,
|
||||
format!("Memory: {}", human_bytes(self.memory as f64)),
|
||||
format!("Architecture: {}", self.architecture),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
itertools = "0.11"
|
||||
menu = { path = "../menu" }
|
||||
picker = { path = "../picker" }
|
||||
postage.workspace = true
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#[cfg(test)]
|
||||
mod file_finder_tests;
|
||||
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||
use std::{
|
||||
@@ -64,33 +65,15 @@ impl FileFinder {
|
||||
FoundPath::new(project_path, abs_path)
|
||||
});
|
||||
|
||||
// if exists, bubble the currently opened path to the top
|
||||
let history_items = currently_opened_path
|
||||
.clone()
|
||||
let history_items = workspace
|
||||
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
||||
.into_iter()
|
||||
.chain(
|
||||
workspace
|
||||
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
|
||||
.into_iter()
|
||||
.filter(|(history_path, _)| {
|
||||
Some(history_path)
|
||||
!= currently_opened_path
|
||||
.as_ref()
|
||||
.map(|found_path| &found_path.project)
|
||||
})
|
||||
.filter(|(_, history_abs_path)| {
|
||||
history_abs_path.as_ref()
|
||||
!= currently_opened_path
|
||||
.as_ref()
|
||||
.and_then(|found_path| found_path.absolute.as_ref())
|
||||
})
|
||||
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||
Some(abs_path) => history_file_exists(abs_path),
|
||||
None => true,
|
||||
})
|
||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||
)
|
||||
.collect();
|
||||
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||
Some(abs_path) => history_file_exists(abs_path),
|
||||
None => true,
|
||||
})
|
||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let weak_workspace = cx.view().downgrade();
|
||||
@@ -139,7 +122,7 @@ pub struct FileFinderDelegate {
|
||||
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
|
||||
currently_opened_path: Option<FoundPath>,
|
||||
matches: Matches,
|
||||
selected_index: Option<usize>,
|
||||
selected_index: usize,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
history_items: Vec<FoundPath>,
|
||||
}
|
||||
@@ -209,31 +192,21 @@ impl Matches {
|
||||
fn push_new_matches(
|
||||
&mut self,
|
||||
history_items: &Vec<FoundPath>,
|
||||
currently_opened: Option<&FoundPath>,
|
||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
|
||||
extend_old_matches: bool,
|
||||
) {
|
||||
let matching_history_paths = matching_history_item_paths(history_items, query);
|
||||
let matching_history_paths =
|
||||
matching_history_item_paths(history_items, currently_opened, query);
|
||||
let new_search_matches = new_search_matches
|
||||
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
||||
let history_items_to_show = history_items.iter().filter_map(|history_item| {
|
||||
Some((
|
||||
history_item.clone(),
|
||||
Some(
|
||||
matching_history_paths
|
||||
.get(&history_item.project.path)?
|
||||
.clone(),
|
||||
),
|
||||
))
|
||||
});
|
||||
self.history.clear();
|
||||
util::extend_sorted(
|
||||
&mut self.history,
|
||||
history_items_to_show,
|
||||
100,
|
||||
|(_, a), (_, b)| b.cmp(a),
|
||||
);
|
||||
|
||||
self.set_new_history(
|
||||
currently_opened,
|
||||
Some(&matching_history_paths),
|
||||
history_items,
|
||||
);
|
||||
if extend_old_matches {
|
||||
self.search
|
||||
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
|
||||
@@ -242,14 +215,52 @@ impl Matches {
|
||||
}
|
||||
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
|
||||
}
|
||||
|
||||
fn set_new_history<'a>(
|
||||
&mut self,
|
||||
currently_opened: Option<&'a FoundPath>,
|
||||
query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
|
||||
history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
|
||||
) {
|
||||
let mut processed_paths = HashSet::default();
|
||||
self.history = history_items
|
||||
.into_iter()
|
||||
.chain(currently_opened)
|
||||
.filter(|&path| processed_paths.insert(path))
|
||||
.filter_map(|history_item| match &query_matches {
|
||||
Some(query_matches) => Some((
|
||||
history_item.clone(),
|
||||
Some(query_matches.get(&history_item.project.path)?.clone()),
|
||||
)),
|
||||
None => Some((history_item.clone(), None)),
|
||||
})
|
||||
.enumerate()
|
||||
.sorted_by(
|
||||
|(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
|
||||
Some(path_a) == currently_opened,
|
||||
Some(path_b) == currently_opened,
|
||||
) {
|
||||
// bubble currently opened files to the top
|
||||
(true, false) => cmp::Ordering::Less,
|
||||
(false, true) => cmp::Ordering::Greater,
|
||||
// arrange the files by their score (best score on top) and by their occurrence in the history
|
||||
// (history items visited later are on the top)
|
||||
_ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
|
||||
},
|
||||
)
|
||||
.map(|(_, paths)| paths)
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
fn matching_history_item_paths(
|
||||
history_items: &Vec<FoundPath>,
|
||||
currently_opened: Option<&FoundPath>,
|
||||
query: &PathLikeWithPosition<FileSearchQuery>,
|
||||
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
|
||||
let history_items_by_worktrees = history_items
|
||||
.iter()
|
||||
.chain(currently_opened)
|
||||
.filter_map(|found_path| {
|
||||
let candidate = PathMatchCandidate {
|
||||
path: &found_path.project.path,
|
||||
@@ -301,7 +312,7 @@ fn matching_history_item_paths(
|
||||
matching_history_paths
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
struct FoundPath {
|
||||
project: ProjectPath,
|
||||
absolute: Option<PathBuf>,
|
||||
@@ -372,7 +383,7 @@ impl FileFinderDelegate {
|
||||
latest_search_query: None,
|
||||
currently_opened_path,
|
||||
matches: Matches::default(),
|
||||
selected_index: None,
|
||||
selected_index: 0,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
history_items,
|
||||
}
|
||||
@@ -427,7 +438,6 @@ impl FileFinderDelegate {
|
||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.selected_index.take();
|
||||
picker
|
||||
.delegate
|
||||
.set_search_matches(search_id, did_cancel, query, matches, cx)
|
||||
@@ -454,12 +464,14 @@ impl FileFinderDelegate {
|
||||
.map(|query| query.path_like.path_query());
|
||||
self.matches.push_new_matches(
|
||||
&self.history_items,
|
||||
self.currently_opened_path.as_ref(),
|
||||
&query,
|
||||
matches.into_iter(),
|
||||
extend_old_matches,
|
||||
);
|
||||
self.latest_search_query = Some(query);
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
self.selected_index = self.calculate_selected_index();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -630,6 +642,19 @@ impl FileFinderDelegate {
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
/// Skips first history match (that is displayed topmost) if it's currently opened.
|
||||
fn calculate_selected_index(&self) -> usize {
|
||||
if let Some(Match::History(path, _)) = self.matches.get(0) {
|
||||
if Some(path) == self.currently_opened_path.as_ref() {
|
||||
let elements_after_first = self.matches.len() - 1;
|
||||
if elements_after_first > 0 {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileFinderDelegate {
|
||||
@@ -644,11 +669,11 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index.unwrap_or(0)
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = Some(ix);
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -671,22 +696,22 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
if raw_query.is_empty() {
|
||||
let project = self.project.read(cx);
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.selected_index.take();
|
||||
self.matches = Matches {
|
||||
history: self
|
||||
.history_items
|
||||
.iter()
|
||||
.filter(|history_item| {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.map(|p| (p, None))
|
||||
.collect(),
|
||||
history: Vec::new(),
|
||||
search: Vec::new(),
|
||||
};
|
||||
self.matches.set_new_history(
|
||||
self.currently_opened_path.as_ref(),
|
||||
None,
|
||||
self.history_items.iter().filter(|history_item| {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
}),
|
||||
);
|
||||
|
||||
self.selected_index = self.calculate_selected_index();
|
||||
cx.notify();
|
||||
Task::ready(())
|
||||
} else {
|
||||
|
||||
@@ -1062,6 +1062,177 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test": {
|
||||
"1_qw": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
// Open new buffer
|
||||
open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_match_selection(&finder, 0, "1_qw");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"bar.rs": "// Bar file",
|
||||
"lib.rs": "// Lib file",
|
||||
"maaa.rs": "// Maaaaaaa",
|
||||
"main.rs": "// Main file",
|
||||
"moo.rs": "// Moooooo",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
|
||||
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
|
||||
open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
|
||||
|
||||
// main.rs is on top, previously used is selected
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "lib.rs");
|
||||
assert_match_at_position(finder, 2, "bar.rs");
|
||||
});
|
||||
|
||||
// all files match, main.rs is still on top
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches(".rs".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 5);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "bar.rs");
|
||||
});
|
||||
|
||||
// main.rs is not among matches, select top item
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("b".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 2);
|
||||
assert_match_at_position(finder, 0, "bar.rs");
|
||||
});
|
||||
|
||||
// main.rs is back, put it on top and select next item
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("m".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "moo.rs");
|
||||
});
|
||||
|
||||
// get back to the initial state
|
||||
picker
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate.update_matches("".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "main.rs");
|
||||
assert_match_selection(finder, 1, "lib.rs");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"test": {
|
||||
"1.txt": "// One",
|
||||
"2.txt": "// Two",
|
||||
"3.txt": "// Three",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
|
||||
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
|
||||
open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "3.txt");
|
||||
assert_match_selection(finder, 1, "2.txt");
|
||||
assert_match_at_position(finder, 2, "1.txt");
|
||||
});
|
||||
|
||||
cx.dispatch_action(Confirm); // Open 2.txt
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "2.txt");
|
||||
assert_match_selection(finder, 1, "3.txt");
|
||||
assert_match_at_position(finder, 2, "1.txt");
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(Confirm); // Open 1.txt
|
||||
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
picker.update(cx, |finder, _| {
|
||||
assert_eq!(finder.delegate.matches.len(), 3);
|
||||
assert_match_at_position(finder, 0, "1.txt");
|
||||
assert_match_selection(finder, 1, "2.txt");
|
||||
assert_match_at_position(finder, 2, "3.txt");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
@@ -1172,6 +1343,27 @@ async fn open_close_queried_buffer(
|
||||
expected_editor_title: &str,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> Vec<FoundPath> {
|
||||
let history_items = open_queried_buffer(
|
||||
input,
|
||||
expected_matches,
|
||||
expected_editor_title,
|
||||
workspace,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
|
||||
|
||||
history_items
|
||||
}
|
||||
|
||||
async fn open_queried_buffer(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
expected_editor_title: &str,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> Vec<FoundPath> {
|
||||
let picker = open_file_picker(&workspace, cx);
|
||||
cx.simulate_input(input);
|
||||
@@ -1186,7 +1378,6 @@ async fn open_close_queried_buffer(
|
||||
finder.delegate.history_items.clone()
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(Confirm);
|
||||
|
||||
cx.read(|cx| {
|
||||
@@ -1198,8 +1389,6 @@ async fn open_close_queried_buffer(
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
|
||||
|
||||
history_items
|
||||
}
|
||||
|
||||
@@ -1313,3 +1502,37 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_match_selection(
|
||||
finder: &Picker<FileFinderDelegate>,
|
||||
expected_selection_index: usize,
|
||||
expected_file_name: &str,
|
||||
) {
|
||||
assert_eq!(
|
||||
finder.delegate.selected_index(),
|
||||
expected_selection_index,
|
||||
"Match is not selected"
|
||||
);
|
||||
assert_match_at_position(finder, expected_selection_index, expected_file_name);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_match_at_position(
|
||||
finder: &Picker<FileFinderDelegate>,
|
||||
match_index: usize,
|
||||
expected_file_name: &str,
|
||||
) {
|
||||
let match_item = finder
|
||||
.delegate
|
||||
.matches
|
||||
.get(match_index)
|
||||
.unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
|
||||
let match_file_name = match match_item {
|
||||
Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
|
||||
Match::Search(path_match) => path_match.0.path.file_name(),
|
||||
}
|
||||
.unwrap()
|
||||
.to_string_lossy();
|
||||
assert_eq!(match_file_name, expected_file_name);
|
||||
}
|
||||
|
||||
@@ -652,27 +652,20 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for window in self.windows.values() {
|
||||
if let Some(window) = window.as_ref() {
|
||||
if window.dirty {
|
||||
window.platform_window.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
for window in self
|
||||
.windows
|
||||
.values()
|
||||
.filter_map(|window| {
|
||||
let window = window.as_ref()?;
|
||||
(window.dirty || window.focus_invalidated).then_some(window.handle)
|
||||
window.dirty.get().then_some(window.handle)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
self.update_window(window, |_, cx| cx.draw()).unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if self.pending_effects.is_empty() {
|
||||
break;
|
||||
}
|
||||
@@ -749,7 +742,7 @@ impl AppContext {
|
||||
fn apply_refresh_effect(&mut self) {
|
||||
for window in self.windows.values_mut() {
|
||||
if let Some(window) = window.as_mut() {
|
||||
window.dirty = true;
|
||||
window.dirty.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::{
|
||||
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
|
||||
ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform,
|
||||
Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
|
||||
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
|
||||
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt};
|
||||
@@ -236,6 +237,11 @@ impl TestAppContext {
|
||||
self.test_platform.has_pending_prompt()
|
||||
}
|
||||
|
||||
/// All the urls that have been opened with cx.open_url() during this test.
|
||||
pub fn opened_url(&self) -> Option<String> {
|
||||
self.test_platform.opened_url.borrow().clone()
|
||||
}
|
||||
|
||||
/// Simulates the user resizing the window to the new size.
|
||||
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
|
||||
self.test_window(window_handle).simulate_resize(size);
|
||||
@@ -625,6 +631,36 @@ impl<'a> VisualTestContext {
|
||||
self.cx.simulate_input(self.window, input)
|
||||
}
|
||||
|
||||
/// Simulate a mouse move event to the given point
|
||||
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
|
||||
self.simulate_event(MouseMoveEvent {
|
||||
position,
|
||||
modifiers,
|
||||
pressed_button: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate a primary mouse click at the given point
|
||||
pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
|
||||
self.simulate_event(MouseDownEvent {
|
||||
position,
|
||||
modifiers,
|
||||
button: MouseButton::Left,
|
||||
click_count: 1,
|
||||
});
|
||||
self.simulate_event(MouseUpEvent {
|
||||
position,
|
||||
modifiers,
|
||||
button: MouseButton::Left,
|
||||
click_count: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/// Simulate a modifiers changed event
|
||||
pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
|
||||
self.simulate_event(ModifiersChangedEvent { modifiers })
|
||||
}
|
||||
|
||||
/// Simulates the user resizing the window to the new size.
|
||||
pub fn simulate_resize(&self, size: Size<Pixels>) {
|
||||
self.simulate_window_resize(self.window, size)
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
|
||||
SharedString, Size, Task, TaskLabel, WindowContext,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use async_task::Runnable;
|
||||
use futures::channel::oneshot;
|
||||
use parking::Unparker;
|
||||
@@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
any::Any,
|
||||
fmt::{self, Debug, Display},
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
@@ -39,6 +38,7 @@ pub(crate) use mac::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) use test::*;
|
||||
use time::UtcOffset;
|
||||
pub use util::SemanticVersion;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
@@ -175,7 +175,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>);
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
|
||||
fn invalidate(&self);
|
||||
fn draw(&self, scene: &Scene);
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
@@ -697,45 +696,6 @@ impl Default for CursorStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// A datastructure representing a semantic version number
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct SemanticVersion {
|
||||
major: usize,
|
||||
minor: usize,
|
||||
patch: usize,
|
||||
}
|
||||
|
||||
impl FromStr for SemanticVersion {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut components = s.trim().split('.');
|
||||
let major = components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing major version number"))?
|
||||
.parse()?;
|
||||
let minor = components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing minor version number"))?
|
||||
.parse()?;
|
||||
let patch = components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing patch version number"))?
|
||||
.parse()?;
|
||||
Ok(Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SemanticVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipboard item that should be copied to the clipboard
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardItem {
|
||||
|
||||
@@ -170,4 +170,34 @@ impl Modifiers {
|
||||
pub fn modified(&self) -> bool {
|
||||
self.control || self.alt || self.shift || self.command || self.function
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with no modifiers
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just command
|
||||
pub fn command() -> Modifiers {
|
||||
Modifiers {
|
||||
command: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with just shift
|
||||
pub fn shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with command + shift
|
||||
pub fn command_shift() -> Modifiers {
|
||||
Modifiers {
|
||||
shift: true,
|
||||
command: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
|
||||
Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
base::{NO, YES},
|
||||
foundation::NSUInteger,
|
||||
@@ -14,17 +15,19 @@ use foreign_types::ForeignType;
|
||||
use media::core_video::CVMetalTextureCache;
|
||||
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
use objc::{self, msg_send, sel, sel_impl};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::{ffi::c_void, mem, ptr, sync::Arc};
|
||||
use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
|
||||
|
||||
#[cfg(not(feature = "runtime_shaders"))]
|
||||
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
||||
#[cfg(feature = "runtime_shaders")]
|
||||
const SHADERS_SOURCE_FILE: &'static str =
|
||||
include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
|
||||
const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
|
||||
const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
|
||||
|
||||
pub(crate) struct MetalRenderer {
|
||||
device: metal::Device,
|
||||
layer: metal::MetalLayer,
|
||||
command_queue: CommandQueue,
|
||||
paths_rasterization_pipeline_state: metal::RenderPipelineState,
|
||||
@@ -36,13 +39,14 @@ pub(crate) struct MetalRenderer {
|
||||
polychrome_sprites_pipeline_state: metal::RenderPipelineState,
|
||||
surfaces_pipeline_state: metal::RenderPipelineState,
|
||||
unit_vertices: metal::Buffer,
|
||||
instances: metal::Buffer,
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
|
||||
sprite_atlas: Arc<MetalAtlas>,
|
||||
core_video_texture_cache: CVMetalTextureCache,
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(is_opaque: bool) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
|
||||
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
|
||||
device
|
||||
} else {
|
||||
@@ -54,7 +58,7 @@ impl MetalRenderer {
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_presents_with_transaction(true);
|
||||
layer.set_opaque(is_opaque);
|
||||
layer.set_opaque(true);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -93,10 +97,6 @@ impl MetalRenderer {
|
||||
mem::size_of_val(&unit_vertices) as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
let instances = device.new_buffer(
|
||||
INSTANCE_BUFFER_SIZE as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
);
|
||||
|
||||
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
|
||||
&device,
|
||||
@@ -165,8 +165,11 @@ impl MetalRenderer {
|
||||
|
||||
let command_queue = device.new_command_queue();
|
||||
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
|
||||
let core_video_texture_cache =
|
||||
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
|
||||
|
||||
Self {
|
||||
device,
|
||||
layer,
|
||||
command_queue,
|
||||
paths_rasterization_pipeline_state,
|
||||
@@ -178,9 +181,9 @@ impl MetalRenderer {
|
||||
polychrome_sprites_pipeline_state,
|
||||
surfaces_pipeline_state,
|
||||
unit_vertices,
|
||||
instances,
|
||||
instance_buffer_pool,
|
||||
sprite_atlas,
|
||||
core_video_texture_cache: unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() },
|
||||
core_video_texture_cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,14 +211,24 @@ impl MetalRenderer {
|
||||
);
|
||||
return;
|
||||
};
|
||||
let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| {
|
||||
self.device.new_buffer(
|
||||
INSTANCE_BUFFER_SIZE as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
)
|
||||
});
|
||||
let command_queue = self.command_queue.clone();
|
||||
let command_buffer = command_queue.new_command_buffer();
|
||||
let mut instance_offset = 0;
|
||||
|
||||
let Some(path_tiles) =
|
||||
self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
|
||||
else {
|
||||
panic!("failed to rasterize {} paths", scene.paths().len());
|
||||
let Some(path_tiles) = self.rasterize_paths(
|
||||
scene.paths(),
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
command_buffer,
|
||||
) else {
|
||||
log::error!("failed to rasterize {} paths", scene.paths().len());
|
||||
return;
|
||||
};
|
||||
|
||||
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||
@@ -243,22 +256,29 @@ impl MetalRenderer {
|
||||
let ok = match batch {
|
||||
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
|
||||
shadows,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Quads(quads) => self.draw_quads(
|
||||
quads,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Quads(quads) => {
|
||||
self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
|
||||
}
|
||||
PrimitiveBatch::Paths(paths) => self.draw_paths(
|
||||
paths,
|
||||
&path_tiles,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
|
||||
underlines,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -269,6 +289,7 @@ impl MetalRenderer {
|
||||
} => self.draw_monochrome_sprites(
|
||||
texture_id,
|
||||
sprites,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -279,12 +300,14 @@ impl MetalRenderer {
|
||||
} => self.draw_polychrome_sprites(
|
||||
texture_id,
|
||||
sprites,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
|
||||
surfaces,
|
||||
&mut instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -292,7 +315,7 @@ impl MetalRenderer {
|
||||
};
|
||||
|
||||
if !ok {
|
||||
panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
@@ -300,28 +323,39 @@ impl MetalRenderer {
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
command_encoder.end_encoding();
|
||||
|
||||
self.instances.did_modify_range(NSRange {
|
||||
instance_buffer.did_modify_range(NSRange {
|
||||
location: 0,
|
||||
length: instance_offset as NSUInteger,
|
||||
});
|
||||
|
||||
let instance_buffer_pool = self.instance_buffer_pool.clone();
|
||||
let instance_buffer = Cell::new(Some(instance_buffer));
|
||||
let block = ConcreteBlock::new(move |_| {
|
||||
if let Some(instance_buffer) = instance_buffer.take() {
|
||||
instance_buffer_pool.lock().push(instance_buffer);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
command_buffer.add_completed_handler(&block);
|
||||
command_buffer.commit();
|
||||
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
command_buffer.wait_until_completed();
|
||||
command_buffer.wait_until_scheduled();
|
||||
drawable.present();
|
||||
}
|
||||
|
||||
fn rasterize_paths(
|
||||
&mut self,
|
||||
paths: &[Path<ScaledPixels>],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
) -> Option<HashMap<PathId, AtlasTile>> {
|
||||
let mut tiles = HashMap::default();
|
||||
@@ -347,9 +381,9 @@ impl MetalRenderer {
|
||||
}
|
||||
|
||||
for (texture_id, vertices) in vertices_by_texture_id {
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
|
||||
let next_offset = *offset + vertices_bytes_len;
|
||||
let next_offset = *instance_offset + vertices_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return None;
|
||||
}
|
||||
@@ -369,8 +403,8 @@ impl MetalRenderer {
|
||||
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
PathRasterizationInputIndex::Vertices as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
let texture_size = Size {
|
||||
width: DevicePixels::from(texture.width()),
|
||||
@@ -382,7 +416,8 @@ impl MetalRenderer {
|
||||
&texture_size as *const Size<DevicePixels> as *const _,
|
||||
);
|
||||
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
vertices.as_ptr() as *const u8,
|
||||
@@ -397,7 +432,7 @@ impl MetalRenderer {
|
||||
vertices.len() as u64,
|
||||
);
|
||||
command_encoder.end_encoding();
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
}
|
||||
|
||||
Some(tiles)
|
||||
@@ -406,14 +441,15 @@ impl MetalRenderer {
|
||||
fn draw_shadows(
|
||||
&mut self,
|
||||
shadows: &[Shadow],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
if shadows.is_empty() {
|
||||
return true;
|
||||
}
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
@@ -423,13 +459,13 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
ShadowInputIndex::Shadows as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
ShadowInputIndex::Shadows as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -439,9 +475,10 @@ impl MetalRenderer {
|
||||
);
|
||||
|
||||
let shadow_bytes_len = mem::size_of_val(shadows);
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *offset + shadow_bytes_len;
|
||||
let next_offset = *instance_offset + shadow_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
@@ -460,21 +497,22 @@ impl MetalRenderer {
|
||||
6,
|
||||
shadows.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
true
|
||||
}
|
||||
|
||||
fn draw_quads(
|
||||
&mut self,
|
||||
quads: &[Quad],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
if quads.is_empty() {
|
||||
return true;
|
||||
}
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
@@ -484,13 +522,13 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
QuadInputIndex::Quads as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
QuadInputIndex::Quads as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -500,9 +538,10 @@ impl MetalRenderer {
|
||||
);
|
||||
|
||||
let quad_bytes_len = mem::size_of_val(quads);
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *offset + quad_bytes_len;
|
||||
let next_offset = *instance_offset + quad_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
@@ -517,7 +556,7 @@ impl MetalRenderer {
|
||||
6,
|
||||
quads.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -525,7 +564,8 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
paths: &[Path<ScaledPixels>],
|
||||
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
@@ -573,7 +613,7 @@ impl MetalRenderer {
|
||||
if sprites.is_empty() {
|
||||
break;
|
||||
} else {
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
let texture_id = prev_texture_id.take().unwrap();
|
||||
let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
|
||||
let texture_size = size(
|
||||
@@ -583,8 +623,8 @@ impl MetalRenderer {
|
||||
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
SpriteInputIndex::AtlasTextureSize as u64,
|
||||
@@ -593,20 +633,20 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder
|
||||
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
|
||||
|
||||
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
|
||||
let next_offset = *offset + sprite_bytes_len;
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
let buffer_contents =
|
||||
unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
@@ -622,7 +662,7 @@ impl MetalRenderer {
|
||||
6,
|
||||
sprites.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
sprites.clear();
|
||||
}
|
||||
}
|
||||
@@ -632,14 +672,15 @@ impl MetalRenderer {
|
||||
fn draw_underlines(
|
||||
&mut self,
|
||||
underlines: &[Underline],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
if underlines.is_empty() {
|
||||
return true;
|
||||
}
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
|
||||
command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
@@ -649,13 +690,13 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
UnderlineInputIndex::Underlines as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
UnderlineInputIndex::Underlines as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -665,9 +706,10 @@ impl MetalRenderer {
|
||||
);
|
||||
|
||||
let underline_bytes_len = mem::size_of_val(underlines);
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *offset + underline_bytes_len;
|
||||
let next_offset = *instance_offset + underline_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
@@ -686,7 +728,7 @@ impl MetalRenderer {
|
||||
6,
|
||||
underlines.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -694,14 +736,15 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
texture_id: AtlasTextureId,
|
||||
sprites: &[MonochromeSprite],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
if sprites.is_empty() {
|
||||
return true;
|
||||
}
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
|
||||
let texture = self.sprite_atlas.metal_texture(texture_id);
|
||||
let texture_size = size(
|
||||
@@ -716,8 +759,8 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
SpriteInputIndex::ViewportSize as u64,
|
||||
@@ -731,15 +774,16 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
|
||||
|
||||
let sprite_bytes_len = mem::size_of_val(sprites);
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *offset + sprite_bytes_len;
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
@@ -758,7 +802,7 @@ impl MetalRenderer {
|
||||
6,
|
||||
sprites.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -766,14 +810,15 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
texture_id: AtlasTextureId,
|
||||
sprites: &[PolychromeSprite],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
if sprites.is_empty() {
|
||||
return true;
|
||||
}
|
||||
align_offset(offset);
|
||||
align_offset(instance_offset);
|
||||
|
||||
let texture = self.sprite_atlas.metal_texture(texture_id);
|
||||
let texture_size = size(
|
||||
@@ -788,8 +833,8 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
SpriteInputIndex::ViewportSize as u64,
|
||||
@@ -803,15 +848,16 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
|
||||
|
||||
let sprite_bytes_len = mem::size_of_val(sprites);
|
||||
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *offset + sprite_bytes_len;
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
@@ -830,14 +876,15 @@ impl MetalRenderer {
|
||||
6,
|
||||
sprites.len() as u64,
|
||||
);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
true
|
||||
}
|
||||
|
||||
fn draw_surfaces(
|
||||
&mut self,
|
||||
surfaces: &[Surface],
|
||||
offset: &mut usize,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
) -> bool {
|
||||
@@ -889,16 +936,16 @@ impl MetalRenderer {
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
align_offset(offset);
|
||||
let next_offset = *offset + mem::size_of::<Surface>();
|
||||
align_offset(instance_offset);
|
||||
let next_offset = *instance_offset + mem::size_of::<Surface>();
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
command_encoder.set_vertex_buffer(
|
||||
SurfaceInputIndex::Surfaces as u64,
|
||||
Some(&self.instances),
|
||||
*offset as u64,
|
||||
Some(instance_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
SurfaceInputIndex::TextureSize as u64,
|
||||
@@ -915,8 +962,8 @@ impl MetalRenderer {
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let buffer_contents =
|
||||
(self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
|
||||
let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
|
||||
as *mut SurfaceBounds;
|
||||
ptr::write(
|
||||
buffer_contents,
|
||||
SurfaceBounds {
|
||||
@@ -927,7 +974,7 @@ impl MetalRenderer {
|
||||
}
|
||||
|
||||
command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
|
||||
*offset = next_offset;
|
||||
*instance_offset = next_offset;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ pub(crate) struct MacPlatformState {
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<MacTextSystem>,
|
||||
display_linker: MacDisplayLinker,
|
||||
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
|
||||
pasteboard: id,
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
@@ -176,6 +177,7 @@ impl MacPlatform {
|
||||
foreground_executor: ForegroundExecutor::new(dispatcher),
|
||||
text_system: Arc::new(MacTextSystem::new()),
|
||||
display_linker: MacDisplayLinker::new(),
|
||||
instance_buffer_pool: Arc::default(),
|
||||
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||
@@ -494,7 +496,13 @@ impl Platform for MacPlatform {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
|
||||
let instance_buffer_pool = self.0.lock().instance_buffer_pool.clone();
|
||||
Box::new(MacWindow::open(
|
||||
handle,
|
||||
options,
|
||||
self.foreground_executor(),
|
||||
instance_buffer_pool,
|
||||
))
|
||||
}
|
||||
|
||||
fn set_display_link_output_callback(
|
||||
|
||||
@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
constant Quad *quads
|
||||
[[buffer(QuadInputIndex_Quads)]]) {
|
||||
Quad quad = quads[input.quad_id];
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
|
||||
quad.corner_radii.top_right == 0. &&
|
||||
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
|
||||
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
|
||||
quad.border_widths.bottom == 0.) {
|
||||
return input.background_color;
|
||||
}
|
||||
|
||||
float2 half_size =
|
||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center =
|
||||
|
||||
@@ -16,8 +16,8 @@ use cocoa::{
|
||||
},
|
||||
base::{id, nil},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
|
||||
NSSize, NSString, NSUInteger,
|
||||
NSArray, NSAutoreleasePool, NSDefaultRunLoopMode, NSDictionary, NSFastEnumeration,
|
||||
NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger,
|
||||
},
|
||||
};
|
||||
use core_graphics::display::CGRect;
|
||||
@@ -168,6 +168,7 @@ unsafe fn build_classes() {
|
||||
sel!(displayLayer:),
|
||||
display_layer as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(sel!(step:), step as extern "C" fn(&Object, Sel, id));
|
||||
|
||||
decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
|
||||
decl.add_method(
|
||||
@@ -260,6 +261,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
sel!(windowDidMove:),
|
||||
window_did_move as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidChangeScreen:),
|
||||
window_did_change_screen as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(windowDidBecomeKey:),
|
||||
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
|
||||
@@ -320,7 +325,8 @@ struct MacWindowState {
|
||||
handle: AnyWindowHandle,
|
||||
executor: ForegroundExecutor,
|
||||
native_window: id,
|
||||
native_view: NonNull<id>,
|
||||
native_view: NonNull<Object>,
|
||||
display_link: id,
|
||||
renderer: MetalRenderer,
|
||||
kind: WindowKind,
|
||||
request_frame_callback: Option<Box<dyn FnMut()>>,
|
||||
@@ -458,6 +464,7 @@ impl MacWindow {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowOptions,
|
||||
executor: ForegroundExecutor,
|
||||
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
|
||||
) -> Self {
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
@@ -521,15 +528,17 @@ impl MacWindow {
|
||||
|
||||
let native_view: id = msg_send![VIEW_CLASS, alloc];
|
||||
let native_view = NSView::init(native_view);
|
||||
|
||||
assert!(!native_view.is_null());
|
||||
|
||||
let display_link = start_display_link(native_window.screen(), native_view);
|
||||
|
||||
let window = Self(Arc::new(Mutex::new(MacWindowState {
|
||||
handle,
|
||||
executor,
|
||||
native_window,
|
||||
native_view: NonNull::new_unchecked(native_view as *mut _),
|
||||
renderer: MetalRenderer::new(true),
|
||||
native_view: NonNull::new_unchecked(native_view),
|
||||
display_link,
|
||||
renderer: MetalRenderer::new(instance_buffer_pool),
|
||||
kind: options.kind,
|
||||
request_frame_callback: None,
|
||||
event_callback: None,
|
||||
@@ -663,6 +672,7 @@ impl MacWindow {
|
||||
}
|
||||
|
||||
window.0.lock().move_traffic_light();
|
||||
|
||||
pool.drain();
|
||||
|
||||
window
|
||||
@@ -683,10 +693,19 @@ impl MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn start_display_link(native_screen: id, native_view: id) -> id {
|
||||
let display_link: id =
|
||||
msg_send![native_screen, displayLinkWithTarget: native_view selector: sel!(step:)];
|
||||
let main_run_loop: id = msg_send![class!(NSRunLoop), mainRunLoop];
|
||||
let _: () = msg_send![display_link, addToRunLoop: main_run_loop forMode: NSDefaultRunLoopMode];
|
||||
display_link
|
||||
}
|
||||
|
||||
impl Drop for MacWindow {
|
||||
fn drop(&mut self) {
|
||||
let this = self.0.lock();
|
||||
let mut this = self.0.lock();
|
||||
let window = this.native_window;
|
||||
this.display_link = nil;
|
||||
this.executor
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
@@ -1000,13 +1019,6 @@ impl PlatformWindow for MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate(&self) {
|
||||
let this = self.0.lock();
|
||||
unsafe {
|
||||
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &crate::Scene) {
|
||||
let mut this = self.0.lock();
|
||||
this.renderer.draw(scene);
|
||||
@@ -1353,6 +1365,19 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
unsafe {
|
||||
let screen = lock.native_window.screen();
|
||||
if screen == nil {
|
||||
lock.display_link = nil;
|
||||
} else {
|
||||
lock.display_link = start_display_link(screen, lock.native_view.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let lock = window_state.lock();
|
||||
@@ -1502,6 +1527,22 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn step(this: &Object, _: Sel, display_link: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.lock();
|
||||
if lock.display_link == display_link {
|
||||
if let Some(mut callback) = lock.request_frame_callback.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
window_state.lock().request_frame_callback = Some(callback);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
let _: () = msg_send![display_link, invalidate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||||
unsafe { msg_send![class!(NSArray), array] }
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub(crate) struct TestPlatform {
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
weak: Weak<Self>,
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ impl TestPlatform {
|
||||
active_window: Default::default(),
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -188,8 +190,8 @@ impl Platform for TestPlatform {
|
||||
|
||||
fn stop_display_link(&self, _display_id: DisplayId) {}
|
||||
|
||||
fn open_url(&self, _url: &str) {
|
||||
unimplemented!()
|
||||
fn open_url(&self, url: &str) {
|
||||
*self.opened_url.borrow_mut() = Some(url.to_string())
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {
|
||||
|
||||
@@ -171,7 +171,7 @@ impl PlatformWindow for TestWindow {
|
||||
}
|
||||
|
||||
fn appearance(&self) -> WindowAppearance {
|
||||
unimplemented!()
|
||||
WindowAppearance::Light
|
||||
}
|
||||
|
||||
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
|
||||
@@ -276,16 +276,12 @@ impl PlatformWindow for TestWindow {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn invalidate(&self) {}
|
||||
|
||||
fn draw(&self, _scene: &crate::Scene) {}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
use anyhow::anyhow;
|
||||
use collections::{BTreeSet, FxHashMap, FxHashSet};
|
||||
use core::fmt;
|
||||
use derive_more::Deref;
|
||||
use itertools::Itertools;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -38,9 +39,8 @@ pub struct FontFamilyId(pub usize);
|
||||
|
||||
pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
|
||||
|
||||
/// The GPUI text layout and rendering sub system.
|
||||
/// The GPUI text rendering sub system.
|
||||
pub struct TextSystem {
|
||||
line_layout_cache: Arc<LineLayoutCache>,
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
|
||||
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
|
||||
@@ -53,7 +53,6 @@ pub struct TextSystem {
|
||||
impl TextSystem {
|
||||
pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
|
||||
TextSystem {
|
||||
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
|
||||
platform_text_system,
|
||||
font_metrics: RwLock::default(),
|
||||
raster_bounds: RwLock::default(),
|
||||
@@ -234,43 +233,66 @@ impl TextSystem {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
|
||||
self.line_layout_cache.with_view(view_id, f)
|
||||
/// Returns a handle to a line wrapper, for the given font and font size.
|
||||
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
|
||||
let lock = &mut self.wrapper_pool.lock();
|
||||
let font_id = self.resolve_font(&font);
|
||||
let wrappers = lock
|
||||
.entry(FontIdWithSize { font_id, font_size })
|
||||
.or_default();
|
||||
let wrapper = wrappers.pop().unwrap_or_else(|| {
|
||||
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
|
||||
});
|
||||
|
||||
LineWrapperHandle {
|
||||
wrapper: Some(wrapper),
|
||||
text_system: self.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout the given line of text, at the given font_size.
|
||||
/// Subsets of the line can be styled independently with the `runs` parameter.
|
||||
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
|
||||
/// can be painted directly.
|
||||
pub fn layout_line(
|
||||
&self,
|
||||
text: &str,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<Arc<LineLayout>> {
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
for run in runs.iter() {
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
if let Some(last_run) = font_runs.last_mut() {
|
||||
if last_run.font_id == font_id {
|
||||
last_run.len += run.len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
font_runs.push(FontRun {
|
||||
len: run.len,
|
||||
font_id,
|
||||
});
|
||||
/// Get the rasterized size and location of a specific, rendered glyph.
|
||||
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let raster_bounds = self.raster_bounds.upgradable_read();
|
||||
if let Some(bounds) = raster_bounds.get(params) {
|
||||
Ok(*bounds)
|
||||
} else {
|
||||
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
|
||||
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
|
||||
raster_bounds.insert(params.clone(), bounds);
|
||||
Ok(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
let layout = self
|
||||
.line_layout_cache
|
||||
.layout_line(text, font_size, &font_runs);
|
||||
pub(crate) fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
let raster_bounds = self.raster_bounds(params)?;
|
||||
self.platform_text_system
|
||||
.rasterize_glyph(params, raster_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
/// The GPUI text layout subsystem.
|
||||
#[derive(Deref)]
|
||||
pub struct WindowTextSystem {
|
||||
line_layout_cache: Arc<LineLayoutCache>,
|
||||
#[deref]
|
||||
text_system: Arc<TextSystem>,
|
||||
}
|
||||
|
||||
Ok(layout)
|
||||
impl WindowTextSystem {
|
||||
pub(crate) fn new(text_system: Arc<TextSystem>) -> Self {
|
||||
Self {
|
||||
line_layout_cache: Arc::new(LineLayoutCache::new(
|
||||
text_system.platform_text_system.clone(),
|
||||
)),
|
||||
text_system,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
|
||||
self.line_layout_cache.with_view(view_id, f)
|
||||
}
|
||||
|
||||
/// Shape the given line, at the given font_size, for painting to the screen.
|
||||
@@ -429,43 +451,39 @@ impl TextSystem {
|
||||
self.line_layout_cache.finish_frame(reused_views)
|
||||
}
|
||||
|
||||
/// Returns a handle to a line wrapper, for the given font and font size.
|
||||
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
|
||||
let lock = &mut self.wrapper_pool.lock();
|
||||
let font_id = self.resolve_font(&font);
|
||||
let wrappers = lock
|
||||
.entry(FontIdWithSize { font_id, font_size })
|
||||
.or_default();
|
||||
let wrapper = wrappers.pop().unwrap_or_else(|| {
|
||||
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
|
||||
});
|
||||
|
||||
LineWrapperHandle {
|
||||
wrapper: Some(wrapper),
|
||||
text_system: self.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the rasterized size and location of a specific, rendered glyph.
|
||||
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let raster_bounds = self.raster_bounds.upgradable_read();
|
||||
if let Some(bounds) = raster_bounds.get(params) {
|
||||
Ok(*bounds)
|
||||
} else {
|
||||
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
|
||||
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
|
||||
raster_bounds.insert(params.clone(), bounds);
|
||||
Ok(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rasterize_glyph(
|
||||
/// Layout the given line of text, at the given font_size.
|
||||
/// Subsets of the line can be styled independently with the `runs` parameter.
|
||||
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
|
||||
/// can be painted directly.
|
||||
pub fn layout_line(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
let raster_bounds = self.raster_bounds(params)?;
|
||||
self.platform_text_system
|
||||
.rasterize_glyph(params, raster_bounds)
|
||||
text: &str,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
) -> Result<Arc<LineLayout>> {
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
for run in runs.iter() {
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
if let Some(last_run) = font_runs.last_mut() {
|
||||
if last_run.font_id == font_id {
|
||||
last_run.len += run.len;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
font_runs.push(FontRun {
|
||||
len: run.len,
|
||||
font_id,
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self
|
||||
.line_layout_cache
|
||||
.layout_line(text, font_size, &font_runs);
|
||||
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
Ok(layout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ impl Boundary {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
|
||||
use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary};
|
||||
use rand::prelude::*;
|
||||
|
||||
#[test]
|
||||
@@ -218,7 +218,7 @@ mod tests {
|
||||
#[crate::test]
|
||||
fn test_wrap_shaped_line(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let text_system = cx.text_system().clone();
|
||||
let text_system = WindowTextSystem::new(cx.text_system().clone());
|
||||
|
||||
let normal = TextRun {
|
||||
len: 0,
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
|
||||
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
|
||||
PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
|
||||
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds,
|
||||
WindowOptions,
|
||||
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowAppearance,
|
||||
WindowBounds, WindowOptions, WindowTextSystem,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::FxHashSet;
|
||||
@@ -22,7 +22,7 @@ use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut},
|
||||
cell::RefCell,
|
||||
cell::{Cell, RefCell},
|
||||
collections::hash_map::Entry,
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
@@ -34,7 +34,7 @@ use std::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::{measure, ResultExt};
|
||||
|
||||
@@ -95,8 +95,8 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
|
||||
|
||||
struct FocusEvent {
|
||||
previous_focus_path: SmallVec<[FocusId; 8]>,
|
||||
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||
old_focus_path: SmallVec<[FocusId; 8]>,
|
||||
new_focus_path: SmallVec<[FocusId; 8]>,
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
@@ -251,6 +251,7 @@ pub struct Window {
|
||||
pub(crate) platform_window: Box<dyn PlatformWindow>,
|
||||
display_id: DisplayId,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
text_system: Arc<WindowTextSystem>,
|
||||
pub(crate) rem_size: Pixels,
|
||||
pub(crate) viewport_size: Size<Pixels>,
|
||||
layout_engine: Option<TaffyLayoutEngine>,
|
||||
@@ -268,17 +269,17 @@ pub struct Window {
|
||||
scale_factor: f32,
|
||||
bounds: WindowBounds,
|
||||
bounds_observers: SubscriberSet<(), AnyObserver>,
|
||||
appearance: WindowAppearance,
|
||||
appearance_observers: SubscriberSet<(), AnyObserver>,
|
||||
active: bool,
|
||||
pub(crate) dirty: bool,
|
||||
pub(crate) dirty: Rc<Cell<bool>>,
|
||||
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) drawing: bool,
|
||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) focus_invalidated: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -337,13 +338,61 @@ impl Window {
|
||||
let content_size = platform_window.content_size();
|
||||
let scale_factor = platform_window.scale_factor();
|
||||
let bounds = platform_window.bounds();
|
||||
let appearance = platform_window.appearance();
|
||||
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
|
||||
let dirty = Rc::new(Cell::new(true));
|
||||
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
|
||||
|
||||
platform_window.on_request_frame(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
let mut cx = AsyncWindowContext::new(cx.to_async(), handle);
|
||||
let dirty = dirty.clone();
|
||||
let last_input_timestamp = last_input_timestamp.clone();
|
||||
move || {
|
||||
measure("frame duration", || {
|
||||
handle.update(&mut cx, |_, cx| cx.draw()).log_err();
|
||||
})
|
||||
fn handle_frame_request(
|
||||
dirty: &Rc<Cell<bool>>,
|
||||
last_input_timestamp: &Rc<Cell<Instant>>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
if dirty.get() {
|
||||
measure("frame duration", || {
|
||||
let original_focus_path =
|
||||
cx.update(|cx| cx.window.rendered_frame.focus_path())?;
|
||||
|
||||
let mut draw_count = 0;
|
||||
while dirty.get() {
|
||||
cx.update(|cx| {
|
||||
let old_window_active = cx.window.rendered_frame.focus_path();
|
||||
let old_focus_path = cx.window.rendered_frame.focus_path();
|
||||
|
||||
cx.draw();
|
||||
cx.notify_focus_listeners(
|
||||
&original_focus_path,
|
||||
old_focus_path,
|
||||
old_window_active,
|
||||
);
|
||||
})?;
|
||||
draw_count += 1;
|
||||
if draw_count > 2 {
|
||||
log::warn!(
|
||||
"redrew frame {} times due to focus changes",
|
||||
draw_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| cx.present())?;
|
||||
Ok(())
|
||||
})?;
|
||||
} else if last_input_timestamp.get().elapsed() < Duration::from_secs(1) {
|
||||
// Keep presenting the current scene for 1 extra second since the
|
||||
// last input to prevent the display from underclocking the refresh rate.
|
||||
cx.update(|cx| cx.present())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
handle_frame_request(&dirty, &last_input_timestamp, &mut cx).log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_resize(Box::new({
|
||||
@@ -362,6 +411,14 @@ impl Window {
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_appearance_changed(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move || {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| cx.appearance_changed())
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_active_status_change(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move |active| {
|
||||
@@ -393,6 +450,7 @@ impl Window {
|
||||
platform_window,
|
||||
display_id,
|
||||
sprite_atlas,
|
||||
text_system,
|
||||
rem_size: px(16.),
|
||||
viewport_size: content_size,
|
||||
layout_engine: Some(TaffyLayoutEngine::new()),
|
||||
@@ -410,17 +468,17 @@ impl Window {
|
||||
scale_factor,
|
||||
bounds,
|
||||
bounds_observers: SubscriberSet::new(),
|
||||
appearance,
|
||||
appearance_observers: SubscriberSet::new(),
|
||||
active: false,
|
||||
dirty: false,
|
||||
dirty,
|
||||
last_input_timestamp,
|
||||
refreshing: false,
|
||||
drawing: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
focus_invalidated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,7 +530,7 @@ impl<'a> WindowContext<'a> {
|
||||
pub fn refresh(&mut self) {
|
||||
if !self.window.drawing {
|
||||
self.window.refreshing = true;
|
||||
self.window.dirty = true;
|
||||
self.window.dirty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,12 +563,6 @@ impl<'a> WindowContext<'a> {
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
self.window.focus_invalidated = true;
|
||||
}
|
||||
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
@@ -530,6 +582,11 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.focus_enabled = false;
|
||||
}
|
||||
|
||||
/// Accessor for the text system.
|
||||
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
|
||||
&self.window.text_system
|
||||
}
|
||||
|
||||
/// Dispatch the given action on the currently focused element.
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
let focus_handle = self.focused();
|
||||
@@ -734,6 +791,20 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.bounds
|
||||
}
|
||||
|
||||
fn appearance_changed(&mut self) {
|
||||
self.window.appearance = self.window.platform_window.appearance();
|
||||
|
||||
self.window
|
||||
.appearance_observers
|
||||
.clone()
|
||||
.retain(&(), |callback| callback(self));
|
||||
}
|
||||
|
||||
/// Returns the appearance of the current window.
|
||||
pub fn appearance(&self) -> WindowAppearance {
|
||||
self.window.appearance
|
||||
}
|
||||
|
||||
/// Returns the size of the drawable area within the window.
|
||||
pub fn viewport_size(&self) -> Size<Pixels> {
|
||||
self.window.viewport_size
|
||||
@@ -927,16 +998,12 @@ impl<'a> WindowContext<'a> {
|
||||
&self.window.next_frame.z_index_stack
|
||||
}
|
||||
|
||||
/// Draw pixels to the display for this window based on the contents of its scene.
|
||||
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
|
||||
/// the contents of the new [Scene], use [present].
|
||||
pub(crate) fn draw(&mut self) {
|
||||
self.window.dirty = false;
|
||||
self.window.dirty.set(false);
|
||||
self.window.drawing = true;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
self.window.focus_invalidated = false;
|
||||
}
|
||||
|
||||
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
|
||||
{
|
||||
let input_handler = self.window.platform_window.take_input_handler();
|
||||
@@ -1035,18 +1102,28 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
element_arena.clear();
|
||||
});
|
||||
|
||||
let previous_focus_path = self.window.rendered_frame.focus_path();
|
||||
let previous_window_active = self.window.rendered_frame.window_active;
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
self.window.next_frame.clear();
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
self.window.refreshing = false;
|
||||
self.window.drawing = false;
|
||||
}
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
fn notify_focus_listeners(
|
||||
&mut self,
|
||||
original_focus_path: &SmallVec<[FocusId; 8]>,
|
||||
old_focus_path: SmallVec<[FocusId; 8]>,
|
||||
old_window_active: bool,
|
||||
) {
|
||||
let new_window_active = self.window.rendered_frame.window_active;
|
||||
let new_focus_path = self.window.rendered_frame.focus_path();
|
||||
|
||||
if new_window_active == old_window_active && new_focus_path == *original_focus_path {
|
||||
log::warn!("skipping focus notifications due to restoring focus to the original handle in a single frame");
|
||||
return;
|
||||
}
|
||||
|
||||
if new_focus_path != old_focus_path || new_window_active != old_window_active {
|
||||
if !old_focus_path.is_empty() && new_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
@@ -1054,15 +1131,15 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
let event = FocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
old_focus_path: if old_window_active {
|
||||
old_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
SmallVec::new()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
new_focus_path: if new_window_active {
|
||||
new_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
SmallVec::new()
|
||||
},
|
||||
};
|
||||
self.window
|
||||
@@ -1070,16 +1147,17 @@ impl<'a> WindowContext<'a> {
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
}
|
||||
}
|
||||
|
||||
fn present(&self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.draw(&self.window.rendered_frame.scene);
|
||||
self.window.refreshing = false;
|
||||
self.window.drawing = false;
|
||||
}
|
||||
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
|
||||
self.window.last_input_timestamp.set(Instant::now());
|
||||
// Handlers may set this to false by calling `stop_propagation`.
|
||||
self.app.propagate_event = true;
|
||||
// Handlers may set this to true by calling `prevent_default`.
|
||||
@@ -2023,7 +2101,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
}
|
||||
|
||||
if !self.window.drawing {
|
||||
self.window_cx.window.dirty = true;
|
||||
self.window_cx.window.dirty.set(true);
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
@@ -2058,6 +2136,20 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Registers a callback to be invoked when the window appearance changes.
|
||||
pub fn observe_window_appearance(
|
||||
&mut self,
|
||||
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
|
||||
) -> Subscription {
|
||||
let view = self.view.downgrade();
|
||||
let (subscription, activate) = self.window.appearance_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Register a listener to be called when the given focus handle receives focus.
|
||||
/// Returns a subscription and persists until the subscription is dropped.
|
||||
pub fn on_focus(
|
||||
@@ -2071,8 +2163,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
(),
|
||||
Box::new(move |event, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.previous_focus_path.last() != Some(&focus_id)
|
||||
&& event.current_focus_path.last() == Some(&focus_id)
|
||||
if event.old_focus_path.last() != Some(&focus_id)
|
||||
&& event.new_focus_path.last() == Some(&focus_id)
|
||||
{
|
||||
listener(view, cx)
|
||||
}
|
||||
@@ -2097,8 +2189,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
(),
|
||||
Box::new(move |event, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if !event.previous_focus_path.contains(&focus_id)
|
||||
&& event.current_focus_path.contains(&focus_id)
|
||||
if !event.old_focus_path.contains(&focus_id)
|
||||
&& event.new_focus_path.contains(&focus_id)
|
||||
{
|
||||
listener(view, cx)
|
||||
}
|
||||
@@ -2123,8 +2215,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
(),
|
||||
Box::new(move |event, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.previous_focus_path.last() == Some(&focus_id)
|
||||
&& event.current_focus_path.last() != Some(&focus_id)
|
||||
if event.old_focus_path.last() == Some(&focus_id)
|
||||
&& event.new_focus_path.last() != Some(&focus_id)
|
||||
{
|
||||
listener(view, cx)
|
||||
}
|
||||
@@ -2166,8 +2258,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
(),
|
||||
Box::new(move |event, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
if event.previous_focus_path.contains(&focus_id)
|
||||
&& !event.current_focus_path.contains(&focus_id)
|
||||
if event.old_focus_path.contains(&focus_id)
|
||||
&& !event.new_focus_path.contains(&focus_id)
|
||||
{
|
||||
listener(view, cx)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ async-trait.workspace = true
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
futures.workspace = true
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
globset.workspace = true
|
||||
gpui = { path = "../gpui" }
|
||||
@@ -38,7 +38,6 @@ log.workspace = true
|
||||
lsp = { path = "../lsp" }
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
rpc = { path = "../rpc" }
|
||||
@@ -55,6 +54,7 @@ text = { path = "../text" }
|
||||
theme = { path = "../theme" }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
pulldown-cmark.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
unicase = "2.6"
|
||||
util = { path = "../util" }
|
||||
|
||||
@@ -2993,6 +2993,11 @@ impl BufferSnapshot {
|
||||
self.git_diff.hunks_intersecting_range_rev(range, self)
|
||||
}
|
||||
|
||||
/// Returns if the buffer contains any diagnostics.
|
||||
pub fn has_diagnostics(&self) -> bool {
|
||||
!self.diagnostics.is_empty()
|
||||
}
|
||||
|
||||
/// Returns all the diagnostics intersecting the given range.
|
||||
pub fn diagnostics_in_range<'a, T, O>(
|
||||
&'a self,
|
||||
|
||||
@@ -30,6 +30,7 @@ workspace = { path = "../workspace" }
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
release_channel = { path = "../release_channel" }
|
||||
env_logger.workspace = true
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -100,6 +100,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init("0.0.0", cx);
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -27,6 +27,7 @@ serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
util = { path = "../util" }
|
||||
release_channel = { path = "../release_channel" }
|
||||
|
||||
[dev-dependencies]
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
|
||||
@@ -5,7 +5,7 @@ pub use lsp_types::*;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashMap;
|
||||
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
|
||||
use gpui::{AsyncAppContext, BackgroundExecutor, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
|
||||
use parking_lot::Mutex;
|
||||
use postage::{barrier, prelude::Stream};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -322,20 +322,54 @@ impl LanguageServer {
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
|
||||
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
|
||||
break;
|
||||
};
|
||||
|
||||
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
|
||||
break;
|
||||
};
|
||||
|
||||
let header = std::str::from_utf8(&buffer)?;
|
||||
let message_len: usize = header
|
||||
let mut segments = header.lines();
|
||||
|
||||
let message_len: usize = segments
|
||||
.next()
|
||||
.with_context(|| {
|
||||
format!("unable to find the first line of the LSP message header `{header}`")
|
||||
})?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
.with_context(|| format!("invalid LSP message header `{header}`"))?
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!("failed to parse Content-Length of LSP message header: `{header}`")
|
||||
})?;
|
||||
|
||||
if let Some(second_segment) = segments.next() {
|
||||
match second_segment {
|
||||
"" => (), // Header end
|
||||
header_field => {
|
||||
if header_field.starts_with("Content-Type:") {
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"inside `{header}`, expected a Content-Type header field or a header ending CRLF, got `{second_segment:?}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"unable to find the second line of the LSP message header `{header}`"
|
||||
);
|
||||
}
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
|
||||
if let Ok(message) = str::from_utf8(&buffer) {
|
||||
log::trace!("incoming message: {}", message);
|
||||
log::trace!("incoming message: {message}");
|
||||
for handler in io_handlers.lock().values_mut() {
|
||||
handler(IoKind::StdOut, message);
|
||||
}
|
||||
@@ -378,6 +412,8 @@ impl LanguageServer {
|
||||
// Don't starve the main thread when receiving lots of messages at once.
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_stderr<Stderr>(
|
||||
@@ -393,7 +429,12 @@ impl LanguageServer {
|
||||
|
||||
loop {
|
||||
buffer.clear();
|
||||
stderr.read_until(b'\n', &mut buffer).await?;
|
||||
|
||||
let bytes_read = stderr.read_until(b'\n', &mut buffer).await?;
|
||||
if bytes_read == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(message) = str::from_utf8(&buffer) {
|
||||
log::trace!("incoming stderr message:{message}");
|
||||
for handler in io_handlers.lock().values_mut() {
|
||||
@@ -450,7 +491,11 @@ impl LanguageServer {
|
||||
/// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned.
|
||||
///
|
||||
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
|
||||
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
|
||||
pub fn initialize(
|
||||
mut self,
|
||||
options: Option<Value>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Arc<Self>>> {
|
||||
let root_uri = Url::from_file_path(&self.root_path).unwrap();
|
||||
#[allow(deprecated)]
|
||||
let params = InitializeParams {
|
||||
@@ -515,7 +560,10 @@ impl LanguageServer {
|
||||
completion_item: Some(CompletionItemCapability {
|
||||
snippet_support: Some(true),
|
||||
resolve_support: Some(CompletionItemCapabilityResolveSupport {
|
||||
properties: vec!["additionalTextEdits".to_string()],
|
||||
properties: vec![
|
||||
"documentation".to_string(),
|
||||
"additionalTextEdits".to_string(),
|
||||
],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -579,18 +627,25 @@ impl LanguageServer {
|
||||
uri: root_uri,
|
||||
name: Default::default(),
|
||||
}]),
|
||||
client_info: None,
|
||||
client_info: Some(ClientInfo {
|
||||
name: release_channel::ReleaseChannel::global(cx)
|
||||
.display_name()
|
||||
.to_string(),
|
||||
version: Some(release_channel::AppVersion::global(cx).to_string()),
|
||||
}),
|
||||
locale: None,
|
||||
};
|
||||
|
||||
let response = self.request::<request::Initialize>(params).await?;
|
||||
if let Some(info) = response.server_info {
|
||||
self.name = info.name;
|
||||
}
|
||||
self.capabilities = response.capabilities;
|
||||
cx.spawn(|_| async move {
|
||||
let response = self.request::<request::Initialize>(params).await?;
|
||||
if let Some(info) = response.server_info {
|
||||
self.name = info.name;
|
||||
}
|
||||
self.capabilities = response.capabilities;
|
||||
|
||||
self.notify::<notification::Initialized>(InitializedParams {})?;
|
||||
Ok(Arc::new(self))
|
||||
self.notify::<notification::Initialized>(InitializedParams {})?;
|
||||
Ok(Arc::new(self))
|
||||
})
|
||||
}
|
||||
|
||||
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
|
||||
@@ -1213,6 +1268,9 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
release_channel::init("0.0.0", cx);
|
||||
});
|
||||
let (server, mut fake) =
|
||||
FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
|
||||
|
||||
@@ -1229,7 +1287,7 @@ mod tests {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let server = server.initialize(None).await.unwrap();
|
||||
let server = cx.update(|cx| server.initialize(None, cx)).await.unwrap();
|
||||
server
|
||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(
|
||||
|
||||
31
crates/markdown_preview/Cargo.toml
Normal file
31
crates/markdown_preview/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/markdown_preview.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
menu = { path = "../menu" }
|
||||
project = { path = "../project" }
|
||||
pulldown-cmark.workspace = true
|
||||
rich_text = { path = "../rich_text" }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
14
crates/markdown_preview/src/markdown_preview.rs
Normal file
14
crates/markdown_preview/src/markdown_preview.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use gpui::{actions, AppContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod markdown_preview_view;
|
||||
pub mod markdown_renderer;
|
||||
|
||||
actions!(markdown, [OpenPreview]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, cx| {
|
||||
markdown_preview_view::MarkdownPreviewView::register(workspace, cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
137
crates/markdown_preview/src/markdown_preview_view.rs
Normal file
137
crates/markdown_preview/src/markdown_preview_view.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use editor::{Editor, EditorEvent};
|
||||
use gpui::{
|
||||
canvas, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, Styled, View, ViewContext,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use workspace::item::Item;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{markdown_renderer::render_markdown, OpenPreview};
|
||||
|
||||
pub struct MarkdownPreviewView {
|
||||
focus_handle: FocusHandle,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
contents: String,
|
||||
}
|
||||
|
||||
impl MarkdownPreviewView {
|
||||
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenPreview, cx| {
|
||||
if workspace.has_active_modal(cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
let languages = languages.clone();
|
||||
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
|
||||
let view: View<MarkdownPreviewView> =
|
||||
cx.new_view(|cx| MarkdownPreviewView::new(editor, languages, cx));
|
||||
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
active_editor: View<Editor>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| {
|
||||
if *event == EditorEvent::Edited {
|
||||
let editor = editor.read(cx);
|
||||
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
||||
this.contents = contents;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let editor = active_editor.read(cx);
|
||||
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
languages,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MarkdownPreviewView {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PreviewEvent {}
|
||||
|
||||
impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
|
||||
|
||||
impl Item for MarkdownPreviewView {
|
||||
type Event = PreviewEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
selected: bool,
|
||||
_cx: &WindowContext,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::FileDoc).color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.child(Label::new("Markdown preview").color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("markdown preview")
|
||||
}
|
||||
|
||||
fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
|
||||
}
|
||||
|
||||
impl Render for MarkdownPreviewView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let rendered_markdown = v_flex()
|
||||
.items_start()
|
||||
.justify_start()
|
||||
.key_context("MarkdownPreview")
|
||||
.track_focus(&self.focus_handle)
|
||||
.id("MarkdownPreview")
|
||||
.overflow_y_scroll()
|
||||
.overflow_x_hidden()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_4()
|
||||
.children(render_markdown(&self.contents, &self.languages, cx));
|
||||
|
||||
div().flex_1().child(
|
||||
// FIXME: This shouldn't be necessary
|
||||
// but the overflow_scroll above doesn't seem to work without it
|
||||
canvas(move |bounds, cx| {
|
||||
rendered_markdown.into_any().draw(
|
||||
bounds.origin,
|
||||
bounds.size.map(AvailableSpace::Definite),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
346
crates/markdown_preview/src/markdown_renderer.rs
Normal file
346
crates/markdown_preview/src/markdown_renderer.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{
|
||||
div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
|
||||
Styled, StyledText, WindowContext,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
|
||||
use rich_text::render_rich_text;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{h_flex, v_flex};
|
||||
|
||||
enum TableState {
|
||||
Header,
|
||||
Body,
|
||||
}
|
||||
|
||||
struct MarkdownTable {
|
||||
column_alignments: Vec<Alignment>,
|
||||
header: Vec<Div>,
|
||||
body: Vec<Vec<Div>>,
|
||||
current_row: Vec<Div>,
|
||||
state: TableState,
|
||||
border_color: Hsla,
|
||||
}
|
||||
|
||||
impl MarkdownTable {
|
||||
fn new(border_color: Hsla, column_alignments: Vec<Alignment>) -> Self {
|
||||
Self {
|
||||
column_alignments,
|
||||
header: Vec::new(),
|
||||
body: Vec::new(),
|
||||
current_row: Vec::new(),
|
||||
state: TableState::Header,
|
||||
border_color,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_row(&mut self) {
|
||||
match self.state {
|
||||
TableState::Header => {
|
||||
self.header.extend(self.current_row.drain(..));
|
||||
self.state = TableState::Body;
|
||||
}
|
||||
TableState::Body => {
|
||||
self.body.push(self.current_row.drain(..).collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_cell(&mut self, contents: AnyElement) {
|
||||
let container = match self.alignment_for_next_cell() {
|
||||
Alignment::Left | Alignment::None => div(),
|
||||
Alignment::Center => v_flex().items_center(),
|
||||
Alignment::Right => v_flex().items_end(),
|
||||
};
|
||||
|
||||
let cell = container
|
||||
.w_full()
|
||||
.child(contents)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_color(self.border_color);
|
||||
|
||||
let cell = match self.state {
|
||||
TableState::Header => cell.border_2(),
|
||||
TableState::Body => cell.border_1(),
|
||||
};
|
||||
|
||||
self.current_row.push(cell);
|
||||
}
|
||||
|
||||
fn finish(self) -> Div {
|
||||
let mut table = v_flex().w_full();
|
||||
let mut header = h_flex();
|
||||
|
||||
for cell in self.header {
|
||||
header = header.child(cell);
|
||||
}
|
||||
table = table.child(header);
|
||||
for row in self.body {
|
||||
let mut row_div = h_flex();
|
||||
for cell in row {
|
||||
row_div = row_div.child(cell);
|
||||
}
|
||||
table = table.child(row_div);
|
||||
}
|
||||
table
|
||||
}
|
||||
|
||||
fn alignment_for_next_cell(&self) -> Alignment {
|
||||
self.column_alignments
|
||||
.get(self.current_row.len())
|
||||
.copied()
|
||||
.unwrap_or(Alignment::None)
|
||||
}
|
||||
}
|
||||
|
||||
struct Renderer<I> {
|
||||
source_contents: String,
|
||||
iter: I,
|
||||
theme: Arc<Theme>,
|
||||
finished: Vec<Div>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
table: Option<MarkdownTable>,
|
||||
list_depth: usize,
|
||||
block_quote_depth: usize,
|
||||
}
|
||||
|
||||
impl<'a, I> Renderer<I>
|
||||
where
|
||||
I: Iterator<Item = (Event<'a>, Range<usize>)>,
|
||||
{
|
||||
fn new(
|
||||
iter: I,
|
||||
source_contents: String,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
theme: Arc<Theme>,
|
||||
) -> Self {
|
||||
Self {
|
||||
iter,
|
||||
source_contents,
|
||||
theme,
|
||||
table: None,
|
||||
finished: vec![],
|
||||
language_registry: language_registry.clone(),
|
||||
list_depth: 0,
|
||||
block_quote_depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, cx: &WindowContext) -> Self {
|
||||
while let Some((event, source_range)) = self.iter.next() {
|
||||
match event {
|
||||
Event::Start(tag) => {
|
||||
self.start_tag(tag);
|
||||
}
|
||||
Event::End(tag) => {
|
||||
self.end_tag(tag, source_range, cx);
|
||||
}
|
||||
Event::Rule => {
|
||||
let rule = div().w_full().h(px(2.)).bg(self.theme.colors().border);
|
||||
self.finished.push(div().mb_4().child(rule));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn start_tag(&mut self, tag: Tag<'a>) {
|
||||
match tag {
|
||||
Tag::List(_) => {
|
||||
self.list_depth += 1;
|
||||
}
|
||||
Tag::BlockQuote => {
|
||||
self.block_quote_depth += 1;
|
||||
}
|
||||
Tag::Table(column_alignments) => {
|
||||
self.table = Some(MarkdownTable::new(
|
||||
self.theme.colors().border,
|
||||
column_alignments,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn end_tag(&mut self, tag: Tag, source_range: Range<usize>, cx: &WindowContext) {
|
||||
match tag {
|
||||
Tag::Paragraph => {
|
||||
if self.list_depth > 0 || self.block_quote_depth > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let element = self.render_md_from_range(source_range.clone(), cx);
|
||||
let paragraph = div().mb_3().child(element);
|
||||
|
||||
self.finished.push(paragraph);
|
||||
}
|
||||
Tag::Heading(level, _, _) => {
|
||||
let mut headline = self.headline(level);
|
||||
if source_range.start > 0 {
|
||||
headline = headline.mt_4();
|
||||
}
|
||||
|
||||
let element = self.render_md_from_range(source_range.clone(), cx);
|
||||
let headline = headline.child(element);
|
||||
|
||||
self.finished.push(headline);
|
||||
}
|
||||
Tag::List(_) => {
|
||||
if self.list_depth == 1 {
|
||||
let element = self.render_md_from_range(source_range.clone(), cx);
|
||||
let list = div().mb_3().child(element);
|
||||
|
||||
self.finished.push(list);
|
||||
}
|
||||
|
||||
self.list_depth -= 1;
|
||||
}
|
||||
Tag::BlockQuote => {
|
||||
let element = self.render_md_from_range(source_range.clone(), cx);
|
||||
|
||||
let block_quote = h_flex()
|
||||
.mb_3()
|
||||
.child(
|
||||
div()
|
||||
.w(px(4.))
|
||||
.bg(self.theme.colors().border)
|
||||
.h_full()
|
||||
.mr_2()
|
||||
.mt_1(),
|
||||
)
|
||||
.text_color(self.theme.colors().text_muted)
|
||||
.child(element);
|
||||
|
||||
self.finished.push(block_quote);
|
||||
|
||||
self.block_quote_depth -= 1;
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
let contents = self.source_contents[source_range.clone()].trim();
|
||||
let contents = contents.trim_start_matches("```");
|
||||
let contents = contents.trim_end_matches("```");
|
||||
let contents = match kind {
|
||||
CodeBlockKind::Fenced(language) => {
|
||||
contents.trim_start_matches(&language.to_string())
|
||||
}
|
||||
CodeBlockKind::Indented => contents,
|
||||
};
|
||||
let contents: String = contents.into();
|
||||
let contents = SharedString::from(contents);
|
||||
|
||||
let code_block = div()
|
||||
.mb_3()
|
||||
.px_4()
|
||||
.py_0()
|
||||
.bg(self.theme.colors().surface_background)
|
||||
.child(StyledText::new(contents));
|
||||
|
||||
self.finished.push(code_block);
|
||||
}
|
||||
Tag::Table(_alignment) => {
|
||||
if self.table.is_none() {
|
||||
log::error!("Table end without table ({:?})", source_range);
|
||||
return;
|
||||
}
|
||||
|
||||
let table = self.table.take().unwrap();
|
||||
let table = table.finish().mb_4();
|
||||
self.finished.push(table);
|
||||
}
|
||||
Tag::TableHead => {
|
||||
if self.table.is_none() {
|
||||
log::error!("Table head without table ({:?})", source_range);
|
||||
return;
|
||||
}
|
||||
|
||||
self.table.as_mut().unwrap().finish_row();
|
||||
}
|
||||
Tag::TableRow => {
|
||||
if self.table.is_none() {
|
||||
log::error!("Table row without table ({:?})", source_range);
|
||||
return;
|
||||
}
|
||||
|
||||
self.table.as_mut().unwrap().finish_row();
|
||||
}
|
||||
Tag::TableCell => {
|
||||
if self.table.is_none() {
|
||||
log::error!("Table cell without table ({:?})", source_range);
|
||||
return;
|
||||
}
|
||||
|
||||
let contents = self.render_md_from_range(source_range.clone(), cx);
|
||||
self.table.as_mut().unwrap().add_cell(contents);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_md_from_range(
|
||||
&self,
|
||||
source_range: Range<usize>,
|
||||
cx: &WindowContext,
|
||||
) -> gpui::AnyElement {
|
||||
let mentions = &[];
|
||||
let language = None;
|
||||
let paragraph = &self.source_contents[source_range.clone()];
|
||||
let rich_text = render_rich_text(
|
||||
paragraph.into(),
|
||||
mentions,
|
||||
&self.language_registry,
|
||||
language,
|
||||
);
|
||||
let id: ElementId = source_range.start.into();
|
||||
rich_text.element(id, cx)
|
||||
}
|
||||
|
||||
fn headline(&self, level: HeadingLevel) -> Div {
|
||||
let size = match level {
|
||||
HeadingLevel::H1 => rems(2.),
|
||||
HeadingLevel::H2 => rems(1.5),
|
||||
HeadingLevel::H3 => rems(1.25),
|
||||
HeadingLevel::H4 => rems(1.),
|
||||
HeadingLevel::H5 => rems(0.875),
|
||||
HeadingLevel::H6 => rems(0.85),
|
||||
};
|
||||
|
||||
let color = match level {
|
||||
HeadingLevel::H6 => self.theme.colors().text_muted,
|
||||
_ => self.theme.colors().text,
|
||||
};
|
||||
|
||||
let line_height = DefiniteLength::from(rems(1.25));
|
||||
|
||||
let headline = h_flex()
|
||||
.w_full()
|
||||
.line_height(line_height)
|
||||
.text_size(size)
|
||||
.text_color(color)
|
||||
.mb_4()
|
||||
.pb(rems(0.15));
|
||||
|
||||
headline
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_markdown(
|
||||
markdown_input: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &WindowContext,
|
||||
) -> Vec<Div> {
|
||||
let theme = cx.theme().clone();
|
||||
let options = Options::all();
|
||||
let parser = Parser::new_ext(markdown_input, options);
|
||||
let renderer = Renderer::new(
|
||||
parser.into_offset_iter(),
|
||||
markdown_input.to_owned(),
|
||||
language_registry,
|
||||
theme,
|
||||
);
|
||||
let renderer = renderer.run(cx);
|
||||
return renderer.finished;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ lsp = { path = "../lsp" }
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
pulldown-cmark.workspace = true
|
||||
rand.workspace = true
|
||||
rich_text = { path = "../rich_text" }
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -3052,6 +3052,12 @@ impl MultiBufferSnapshot {
|
||||
self.has_conflict
|
||||
}
|
||||
|
||||
pub fn has_diagnostics(&self) -> bool {
|
||||
self.excerpts
|
||||
.iter()
|
||||
.any(|excerpt| excerpt.buffer.has_diagnostics())
|
||||
}
|
||||
|
||||
pub fn diagnostic_group<'a, O>(
|
||||
&'a self,
|
||||
group_id: usize,
|
||||
|
||||
@@ -58,13 +58,16 @@ impl<D: PickerDelegate> FocusableView for Picker<D> {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_editor(placeholder: Arc<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor
|
||||
})
|
||||
}
|
||||
impl<D: PickerDelegate> Picker<D> {
|
||||
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text(delegate.placeholder_text(), cx);
|
||||
editor
|
||||
});
|
||||
let editor = create_editor(delegate.placeholder_text(), cx);
|
||||
cx.subscribe(&editor, Self::on_input_editor_event).detach();
|
||||
let mut this = Self {
|
||||
delegate,
|
||||
|
||||
@@ -195,11 +195,11 @@ impl Prettier {
|
||||
},
|
||||
Path::new("/"),
|
||||
None,
|
||||
cx,
|
||||
cx.clone(),
|
||||
)
|
||||
.context("prettier server creation")?;
|
||||
let server = executor
|
||||
.spawn(server.initialize(None))
|
||||
let server = cx
|
||||
.update(|cx| executor.spawn(server.initialize(None, cx)))?
|
||||
.await
|
||||
.context("prettier server initialization")?;
|
||||
Ok(Self::Real(RealPrettier {
|
||||
|
||||
@@ -75,6 +75,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
git2.workspace = true
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
release_channel = { path = "../release_channel" }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
prettier = { path = "../prettier", features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -3130,7 +3130,9 @@ impl Project {
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
let language_server = language_server.initialize(initialization_options).await?;
|
||||
let language_server = cx
|
||||
.update(|cx| language_server.initialize(initialization_options, cx))?
|
||||
.await?;
|
||||
|
||||
language_server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
@@ -4412,13 +4414,13 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn definition<T: ToPointUtf16>(
|
||||
#[inline(never)]
|
||||
fn definition_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
@@ -4426,14 +4428,22 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn type_definition<T: ToPointUtf16>(
|
||||
pub fn definition<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.definition_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn type_definition_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
@@ -4441,7 +4451,30 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
pub fn type_definition<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
|
||||
self.type_definition_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn references_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Location>>> {
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetReferences { position },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
pub fn references<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -4449,10 +4482,19 @@ impl Project {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Location>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.references_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn document_highlights_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<DocumentHighlight>>> {
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetReferences { position },
|
||||
GetDocumentHighlights { position },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -4464,12 +4506,7 @@ impl Project {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<DocumentHighlight>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetDocumentHighlights { position },
|
||||
cx,
|
||||
)
|
||||
self.document_highlights_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
|
||||
@@ -4692,13 +4729,12 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hover<T: ToPointUtf16>(
|
||||
fn hover_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Hover>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
@@ -4706,14 +4742,23 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn completions<T: ToOffset + ToPointUtf16>(
|
||||
pub fn hover<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
) -> Task<Result<Option<Hover>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.hover_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn completions_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
if self.is_local() {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let offset = position.to_offset(&snapshot);
|
||||
@@ -4760,6 +4805,15 @@ impl Project {
|
||||
Task::ready(Ok(Default::default()))
|
||||
}
|
||||
}
|
||||
pub fn completions<T: ToOffset + ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.completions_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn resolve_completions(
|
||||
&self,
|
||||
@@ -5036,6 +5090,20 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn code_actions_impl(
|
||||
&self,
|
||||
buffer_handle: &Model<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.request_lsp(
|
||||
buffer_handle.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetCodeActions { range },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn code_actions<T: Clone + ToOffset>(
|
||||
&self,
|
||||
buffer_handle: &Model<Buffer>,
|
||||
@@ -5044,12 +5112,7 @@ impl Project {
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||
self.request_lsp(
|
||||
buffer_handle.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetCodeActions { range },
|
||||
cx,
|
||||
)
|
||||
self.code_actions_impl(buffer_handle, range, cx)
|
||||
}
|
||||
|
||||
pub fn apply_code_action(
|
||||
@@ -5418,13 +5481,12 @@ impl Project {
|
||||
Ok(project_transaction)
|
||||
}
|
||||
|
||||
pub fn prepare_rename<T: ToPointUtf16>(
|
||||
fn prepare_rename_impl(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: T,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Range<Anchor>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer,
|
||||
LanguageServerToQuery::Primary,
|
||||
@@ -5432,11 +5494,20 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn perform_rename<T: ToPointUtf16>(
|
||||
pub fn prepare_rename<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Range<Anchor>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.prepare_rename_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn perform_rename_impl(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
new_name: String,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -5453,22 +5524,28 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_type_format<T: ToPointUtf16>(
|
||||
pub fn perform_rename<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: T,
|
||||
new_name: String,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.perform_rename_impl(buffer, position, new_name, push_to_history, cx)
|
||||
}
|
||||
|
||||
pub fn on_type_format_impl(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
trigger: String,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
let (position, tab_size) = buffer.update(cx, |buffer, cx| {
|
||||
let position = position.to_point_utf16(buffer);
|
||||
(
|
||||
position,
|
||||
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
|
||||
.tab_size,
|
||||
)
|
||||
let tab_size = buffer.update(cx, |buffer, cx| {
|
||||
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx).tab_size
|
||||
});
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
@@ -5483,6 +5560,18 @@ impl Project {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn on_type_format<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
position: T,
|
||||
trigger: String,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.on_type_format_impl(buffer, position, trigger, push_to_history, cx)
|
||||
}
|
||||
|
||||
pub fn inlay_hints<T: ToOffset>(
|
||||
&self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
@@ -5491,6 +5580,15 @@ impl Project {
|
||||
) -> Task<anyhow::Result<Vec<InlayHint>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||
self.inlay_hints_impl(buffer_handle, range, cx)
|
||||
}
|
||||
fn inlay_hints_impl(
|
||||
&self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<Vec<InlayHint>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let range_start = range.start;
|
||||
let range_end = range.end;
|
||||
let buffer_id = buffer.remote_id().into();
|
||||
|
||||
@@ -4380,6 +4380,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init("0.0.0", cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
@@ -262,11 +262,14 @@ impl ProjectPanel {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||
let file_path = entry.path.clone();
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
|
||||
workspace
|
||||
.open_path(
|
||||
ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: entry.path.clone(),
|
||||
worktree_id,
|
||||
path: file_path.clone(),
|
||||
},
|
||||
None,
|
||||
focus_opened_item,
|
||||
@@ -281,8 +284,16 @@ impl ProjectPanel {
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
if !focus_opened_item {
|
||||
if let Some(project_panel) = project_panel.upgrade() {
|
||||
|
||||
if let Some(project_panel) = project_panel.upgrade() {
|
||||
// Always select the entry, regardless of whether it is opened or not.
|
||||
project_panel.update(cx, |project_panel, _| {
|
||||
project_panel.selection = Some(Selection {
|
||||
worktree_id,
|
||||
entry_id
|
||||
});
|
||||
});
|
||||
if !focus_opened_item {
|
||||
let focus_handle = project_panel.read(cx).focus_handle.clone();
|
||||
cx.focus(&focus_handle);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
release_channel = { path = "../release_channel" }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
@@ -392,6 +392,7 @@ mod tests {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init("0.0.0", cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
|
||||
@@ -14,6 +14,7 @@ assistant = { path = "../assistant" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
ui = { path = "../ui" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use assistant::{AssistantPanel, InlineAssist};
|
||||
use editor::Editor;
|
||||
use editor::{Editor, EditorSettings};
|
||||
|
||||
use gpui::{
|
||||
Action, ClickEvent, 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 workspace::{
|
||||
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
@@ -16,16 +17,26 @@ pub struct QuickActionBar {
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
_inlay_hints_enabled_subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
show: bool,
|
||||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pub fn new(
|
||||
buffer_search_bar: View<BufferSearchBar>,
|
||||
workspace: &Workspace,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
buffer_search_bar,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
show: true,
|
||||
};
|
||||
this.apply_settings(cx);
|
||||
cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
fn active_editor(&self) -> Option<View<Editor>> {
|
||||
@@ -33,6 +44,24 @@ impl QuickActionBar {
|
||||
.as_ref()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
}
|
||||
|
||||
fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
|
||||
if new_show != self.show {
|
||||
self.show = new_show;
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
self.get_toolbar_item_location(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
|
||||
if self.show && self.active_editor().is_some() {
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for QuickActionBar {
|
||||
@@ -40,7 +69,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,
|
||||
@@ -155,36 +183,28 @@ impl ToolbarItemView for QuickActionBar {
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
match active_pane_item {
|
||||
Some(active_item) => {
|
||||
self.active_item = Some(active_item.boxed_clone());
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
|
||||
if let Some(active_item) = active_pane_item {
|
||||
self._inlay_hints_enabled_subscription.take();
|
||||
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|
||||
|| supports_inlay_hints != new_supports_inlay_hints;
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
supports_inlay_hints = new_supports_inlay_hints;
|
||||
if should_notify {
|
||||
cx.notify()
|
||||
}
|
||||
}));
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.active_item = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
if let Some(editor) = active_item.downcast::<Editor>() {
|
||||
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
|
||||
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
|
||||
self._inlay_hints_enabled_subscription =
|
||||
Some(cx.observe(&editor, move |_, editor, cx| {
|
||||
let editor = editor.read(cx);
|
||||
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|
||||
|| supports_inlay_hints != new_supports_inlay_hints;
|
||||
inlay_hints_enabled = new_inlay_hints_enabled;
|
||||
supports_inlay_hints = new_supports_inlay_hints;
|
||||
if should_notify {
|
||||
cx.notify()
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.get_toolbar_item_location()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{AppContext, Global};
|
||||
use gpui::{AppContext, Global, SemanticVersion};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::env;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
|
||||
static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
|
||||
Lazy::new(|| {
|
||||
env::var("ZED_RELEASE_CHANNEL")
|
||||
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
||||
@@ -11,6 +11,7 @@ pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
|
||||
} else {
|
||||
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
|
||||
Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() {
|
||||
@@ -39,6 +40,29 @@ impl AppCommitSha {
|
||||
}
|
||||
}
|
||||
|
||||
struct GlobalAppVersion(SemanticVersion);
|
||||
|
||||
impl Global for GlobalAppVersion {}
|
||||
|
||||
pub struct AppVersion;
|
||||
|
||||
impl AppVersion {
|
||||
pub fn init(pkg_version: &str, cx: &mut AppContext) {
|
||||
let version = if let Some(from_env) = env::var("ZED_APP_VERSION").ok() {
|
||||
from_env.parse().expect("invalid ZED_APP_VERSION")
|
||||
} else {
|
||||
cx.app_metadata()
|
||||
.app_version
|
||||
.unwrap_or_else(|| pkg_version.parse().expect("invalid version in Cargo.toml"))
|
||||
};
|
||||
cx.set_global(GlobalAppVersion(version))
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> SemanticVersion {
|
||||
cx.global::<GlobalAppVersion>().0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
pub enum ReleaseChannel {
|
||||
#[default]
|
||||
@@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel);
|
||||
|
||||
impl Global for GlobalReleaseChannel {}
|
||||
|
||||
impl ReleaseChannel {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
|
||||
}
|
||||
pub fn init(pkg_version: &str, cx: &mut AppContext) {
|
||||
AppVersion::init(pkg_version, cx);
|
||||
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
|
||||
}
|
||||
|
||||
impl ReleaseChannel {
|
||||
pub fn global(cx: &AppContext) -> Self {
|
||||
cx.global::<GlobalReleaseChannel>().0
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ futures.workspace = true
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lazy_static.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
pulldown-cmark.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
||||
@@ -13,6 +13,7 @@ use util::RangeExt;
|
||||
pub enum Highlight {
|
||||
Code,
|
||||
Id(HighlightId),
|
||||
InlineCode(bool),
|
||||
Highlight(HighlightStyle),
|
||||
Mention,
|
||||
SelfMention,
|
||||
@@ -47,7 +48,7 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
|
||||
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let code_background = theme.colors().surface_background;
|
||||
|
||||
@@ -67,6 +68,23 @@ impl RichText {
|
||||
background_color: Some(code_background),
|
||||
..id.style(theme.syntax()).unwrap_or_default()
|
||||
},
|
||||
Highlight::InlineCode(link) => {
|
||||
if !*link {
|
||||
HighlightStyle {
|
||||
background_color: Some(code_background),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
HighlightStyle {
|
||||
background_color: Some(code_background),
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
Highlight::Highlight(highlight) => *highlight,
|
||||
Highlight::Mention => HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
@@ -83,7 +101,12 @@ impl RichText {
|
||||
)
|
||||
.on_click(self.link_ranges.clone(), {
|
||||
let link_urls = self.link_urls.clone();
|
||||
move |ix, cx| cx.open_url(&link_urls[ix])
|
||||
move |ix, cx| {
|
||||
let url = &link_urls[ix];
|
||||
if url.starts_with("http") {
|
||||
cx.open_url(url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.tooltip({
|
||||
let link_ranges = self.link_ranges.clone();
|
||||
@@ -179,22 +202,14 @@ pub fn render_markdown_mut(
|
||||
}
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
if link_url.is_some() {
|
||||
highlights.push((
|
||||
prev_len..text.len(),
|
||||
Highlight::Highlight(HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
let is_link = link_url.is_some();
|
||||
|
||||
if let Some(link_url) = link_url.clone() {
|
||||
link_ranges.push(prev_len..text.len());
|
||||
link_urls.push(link_url);
|
||||
}
|
||||
|
||||
highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
|
||||
}
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||
@@ -256,7 +271,7 @@ pub fn render_markdown_mut(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_markdown(
|
||||
pub fn render_rich_text(
|
||||
block: String,
|
||||
mentions: &[Mention],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
|
||||
@@ -210,7 +210,7 @@ impl SettingsStore {
|
||||
|
||||
if let Some(release_settings) = &self
|
||||
.raw_user_settings
|
||||
.get(&*release_channel::RELEASE_CHANNEL_NAME)
|
||||
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
|
||||
{
|
||||
if let Some(release_settings) = setting_value
|
||||
.deserialize_setting(&release_settings)
|
||||
@@ -543,7 +543,7 @@ impl SettingsStore {
|
||||
|
||||
if let Some(release_settings) = &self
|
||||
.raw_user_settings
|
||||
.get(&*release_channel::RELEASE_CHANNEL_NAME)
|
||||
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
|
||||
{
|
||||
if let Some(release_settings) = setting_value
|
||||
.deserialize_setting(&release_settings)
|
||||
|
||||
@@ -11,7 +11,8 @@ doctest = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
alacritty_terminal = "0.21"
|
||||
# needed for "a few weeks" until alacritty 0.13.2 is out
|
||||
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "2d2b894c3b869fadc78fce9d72cb5c8d2b764cac" }
|
||||
anyhow.workspace = true
|
||||
db = { path = "../db" }
|
||||
dirs = "4.0.0"
|
||||
|
||||
@@ -86,6 +86,15 @@ pub enum Event {
|
||||
Open(MaybeNavigationTarget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PathLikeTarget {
|
||||
/// File system path, absolute or relative, existing or not.
|
||||
/// Might have line and column number(s) attached as `file.rs:1:23`
|
||||
pub maybe_path: String,
|
||||
/// Current working directory of the terminal
|
||||
pub terminal_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// A string inside terminal, potentially useful as a URI that can be opened.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MaybeNavigationTarget {
|
||||
@@ -93,7 +102,7 @@ pub enum MaybeNavigationTarget {
|
||||
Url(String),
|
||||
/// File system path, absolute or relative, existing or not.
|
||||
/// Might have line and column number(s) attached as `file.rs:1:23`
|
||||
PathLike(String),
|
||||
PathLike(PathLikeTarget),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -364,7 +373,7 @@ impl TerminalBuilder {
|
||||
pty,
|
||||
pty_options.hold,
|
||||
false,
|
||||
);
|
||||
)?;
|
||||
|
||||
//Kick things off
|
||||
let pty_tx = event_loop.channel();
|
||||
@@ -626,6 +635,12 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cwd(&self) -> Option<PathBuf> {
|
||||
self.foreground_process_info
|
||||
.as_ref()
|
||||
.map(|info| info.cwd.clone())
|
||||
}
|
||||
|
||||
///Takes events from Alacritty and translates them to behavior on this view
|
||||
fn process_terminal_event(
|
||||
&mut self,
|
||||
@@ -800,7 +815,10 @@ impl Terminal {
|
||||
let target = if is_url {
|
||||
MaybeNavigationTarget::Url(maybe_url_or_path)
|
||||
} else {
|
||||
MaybeNavigationTarget::PathLike(maybe_url_or_path)
|
||||
MaybeNavigationTarget::PathLike(PathLikeTarget {
|
||||
maybe_path: maybe_url_or_path,
|
||||
terminal_dir: self.get_cwd(),
|
||||
})
|
||||
};
|
||||
cx.emit(Event::Open(target));
|
||||
} else {
|
||||
@@ -852,7 +870,10 @@ impl Terminal {
|
||||
let navigation_target = if is_url {
|
||||
MaybeNavigationTarget::Url(word)
|
||||
} else {
|
||||
MaybeNavigationTarget::PathLike(word)
|
||||
MaybeNavigationTarget::PathLike(PathLikeTarget {
|
||||
maybe_path: word,
|
||||
terminal_dir: self.get_cwd(),
|
||||
})
|
||||
};
|
||||
cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user