Compare commits
117 Commits
event-serv
...
fix-node-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3d25b1e0 | ||
|
|
7b6f8c279d | ||
|
|
7a90b1124f | ||
|
|
a5b14de401 | ||
|
|
ba1d28f160 | ||
|
|
2f3102672c | ||
|
|
315e45f543 | ||
|
|
1e18bcb949 | ||
|
|
f2357c71e1 | ||
|
|
42ea2be1b4 | ||
|
|
1732ea95c2 | ||
|
|
3a79aa85f4 | ||
|
|
0b8c1680fb | ||
|
|
097032327d | ||
|
|
7db85b0d2e | ||
|
|
ab7ce32888 | ||
|
|
4e935f9f0f | ||
|
|
2f4890ae39 | ||
|
|
5ddd343b27 | ||
|
|
a9f35d2914 | ||
|
|
410c46a551 | ||
|
|
1f611a9c90 | ||
|
|
84affa96ff | ||
|
|
4386268a94 | ||
|
|
e5a4421559 | ||
|
|
99c6389ff8 | ||
|
|
0325051629 | ||
|
|
11c97a396e | ||
|
|
7fd736e23c | ||
|
|
4dd83da627 | ||
|
|
719e6e9777 | ||
|
|
64ba08cced | ||
|
|
b93e564a78 | ||
|
|
483a735e03 | ||
|
|
a787be6c9f | ||
|
|
b890fa71ff | ||
|
|
9d10969906 | ||
|
|
79098671e6 | ||
|
|
8631280baa | ||
|
|
70888cf3d6 | ||
|
|
5ad8e721db | ||
|
|
4ca6e0e387 | ||
|
|
57b5bff299 | ||
|
|
df3bd40c56 | ||
|
|
23315d214c | ||
|
|
0dd5fe313b | ||
|
|
b9ecca7524 | ||
|
|
b60254feca | ||
|
|
97691c1def | ||
|
|
746223427e | ||
|
|
80caa74866 | ||
|
|
b6189b05f9 | ||
|
|
55f08c0511 | ||
|
|
f8672289fc | ||
|
|
6237e5eb50 | ||
|
|
44105e1f80 | ||
|
|
decfbc69a5 | ||
|
|
6513886867 | ||
|
|
53815af2d2 | ||
|
|
5596a34311 | ||
|
|
fdadbc7174 | ||
|
|
13bbaf1e18 | ||
|
|
c1e291bc96 | ||
|
|
1b261608c6 | ||
|
|
90b631ff3e | ||
|
|
a414b16754 | ||
|
|
9c02239afa | ||
|
|
178ffabca6 | ||
|
|
4ba57d730b | ||
|
|
f2e7c635ac | ||
|
|
8c681d0db3 | ||
|
|
9969d6c702 | ||
|
|
8c8c1769c7 | ||
|
|
58919e9f04 | ||
|
|
a0d7ec9f8e | ||
|
|
ba8aba4d17 | ||
|
|
66e873942d | ||
|
|
cb430fc3e4 | ||
|
|
52c70c1082 | ||
|
|
1651cdf03c | ||
|
|
a1e5f6bb7c | ||
|
|
4ae3396253 | ||
|
|
1c62839295 | ||
|
|
b7cf3040ef | ||
|
|
247825bdd3 | ||
|
|
f7c5d70740 | ||
|
|
f47bd32f15 | ||
|
|
c3c4e37940 | ||
|
|
d3dfa91254 | ||
|
|
4ff1ee126c | ||
|
|
1b9014bca6 | ||
|
|
f42f4432ec | ||
|
|
a59a388c15 | ||
|
|
43d79af94a | ||
|
|
26ffdaffe2 | ||
|
|
266643440c | ||
|
|
8bc41e150e | ||
|
|
8629a076a7 | ||
|
|
3cbac27117 | ||
|
|
1a358e203e | ||
|
|
ba26acc1ed | ||
|
|
edadc6f938 | ||
|
|
3da625e538 | ||
|
|
586f70852e | ||
|
|
3df144c88a | ||
|
|
af79e6b423 | ||
|
|
43be375c76 | ||
|
|
26b5f34046 | ||
|
|
5b2c019f83 | ||
|
|
18b6ded8f0 | ||
|
|
67c9fc575f | ||
|
|
ba4d4c8e1c | ||
|
|
bf4478703b | ||
|
|
748cd38d77 | ||
|
|
1db136ff65 | ||
|
|
0ae0b08c38 | ||
|
|
5b8bb6237f |
15
.cloudflare/README.md
Normal file
15
.cloudflare/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
We have two cloudflare workers that let us serve some assets of this repo
|
||||
from Cloudflare.
|
||||
|
||||
* `open-source-website-assets` is used for `install.sh`
|
||||
* `docs-proxy` is used for `https://zed.dev/docs`
|
||||
|
||||
On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare.
|
||||
|
||||
### Deployment
|
||||
|
||||
These functions are deployed on push to main by the deploy_cloudflare.yml workflow. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers.
|
||||
|
||||
### Testing
|
||||
|
||||
You can use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to test these workers locally, or to deploy custom versions.
|
||||
14
.cloudflare/docs-proxy/src/worker.js
Normal file
14
.cloudflare/docs-proxy/src/worker.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
async fetch(request, _env, _ctx) {
|
||||
const url = new URL(request.url);
|
||||
url.hostname = "docs-anw.pages.dev";
|
||||
|
||||
let res = await fetch(url, request);
|
||||
|
||||
if (res.status === 404) {
|
||||
res = await fetch("https://zed.dev/404");
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
8
.cloudflare/docs-proxy/wrangler.toml
Normal file
8
.cloudflare/docs-proxy/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "docs-proxy"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2024-05-03"
|
||||
workers_dev = true
|
||||
|
||||
[[routes]]
|
||||
pattern = "zed.dev/docs*"
|
||||
zone_name = "zed.dev"
|
||||
19
.cloudflare/open-source-website-assets/src/worker.js
Normal file
19
.cloudflare/open-source-website-assets/src/worker.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const url = new URL(request.url);
|
||||
const key = url.pathname.slice(1);
|
||||
|
||||
const object = await env.OPEN_SOURCE_WEBSITE_ASSETS_BUCKET.get(key);
|
||||
if (!object) {
|
||||
return await fetch("https://zed.dev/404");
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set("etag", object.httpEtag);
|
||||
|
||||
return new Response(object.body, {
|
||||
headers,
|
||||
});
|
||||
},
|
||||
};
|
||||
8
.cloudflare/open-source-website-assets/wrangler.toml
Normal file
8
.cloudflare/open-source-website-assets/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "open-source-website-assets"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2024-05-15"
|
||||
workers_dev = true
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = 'OPEN_SOURCE_WEBSITE_ASSETS_BUCKET'
|
||||
bucket_name = 'zed-open-source-website-assets'
|
||||
56
.github/workflows/deploy_cloudflare.yml
vendored
Normal file
56
.github/workflows/deploy_cloudflare.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
- name: Build book
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p target/deploy
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy Docs
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
|
||||
- name: Deploy Install
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
|
||||
|
||||
- name: Deploy Docs Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
|
||||
- name: Deploy Install Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy .cloudflare/docs-proxy/src/worker.js
|
||||
35
.github/workflows/deploy_docs.yml
vendored
35
.github/workflows/deploy_docs.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
name: Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
mdbook-version: "0.4.37"
|
||||
|
||||
- name: Build book
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p target/deploy
|
||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||
|
||||
- name: Deploy
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy target/deploy --project-name=docs
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@ DerivedData/
|
||||
.venv
|
||||
.blob_store
|
||||
.vscode
|
||||
.wrangler
|
||||
|
||||
@@ -3,10 +3,5 @@
|
||||
"label": "clippy",
|
||||
"command": "cargo",
|
||||
"args": ["xtask", "clippy"]
|
||||
},
|
||||
{
|
||||
"label": "assistant2",
|
||||
"command": "cargo",
|
||||
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
|
||||
}
|
||||
]
|
||||
|
||||
162
Cargo.lock
generated
162
Cargo.lock
generated
@@ -70,6 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.2.10",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
@@ -240,9 +241,9 @@ checksum = "e78f17bacc1bc7b91fef7b1885c10772eb2b9e4e989356f6f0f6a972240f97cd"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.75"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -336,6 +337,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
@@ -359,16 +361,20 @@ dependencies = [
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strsim 0.11.1",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
"tiktoken-rs",
|
||||
"toml 0.8.10",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
@@ -1299,7 +1305,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bitflags 1.3.2",
|
||||
"bytes 1.5.0",
|
||||
"futures-util",
|
||||
@@ -1394,9 +1400,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.4"
|
||||
version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
@@ -1679,7 +1685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.3.8",
|
||||
"regex-automata 0.3.9",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2089,7 +2095,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex 0.2.4",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
@@ -2113,7 +2119,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.5.1",
|
||||
"strsim",
|
||||
"strsim 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2410,7 +2416,6 @@ dependencies = [
|
||||
"call",
|
||||
"channel",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"dev_server_projects",
|
||||
@@ -2559,6 +2564,26 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.10",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
@@ -3727,20 +3752,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event_server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exec"
|
||||
version = "0.3.1"
|
||||
@@ -3848,9 +3859,9 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex",
|
||||
@@ -4622,7 +4633,6 @@ name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"indoc",
|
||||
@@ -4765,6 +4775,7 @@ dependencies = [
|
||||
"windows 0.56.0",
|
||||
"windows-core 0.56.0",
|
||||
"x11rb",
|
||||
"xim",
|
||||
"xkbcommon",
|
||||
]
|
||||
|
||||
@@ -4866,7 +4877,7 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bytes 1.5.0",
|
||||
"headers-core",
|
||||
"http 0.2.9",
|
||||
@@ -6050,8 +6061,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.94.1"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a"
|
||||
version = "0.95.1"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?branch=apply-snippet-edit#853c7881d200777e20799026651ca36727144646"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
@@ -7024,7 +7035,6 @@ dependencies = [
|
||||
name = "outline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -7396,7 +7406,7 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"indexmap 1.9.3",
|
||||
"line-wrap",
|
||||
"quick-xml 0.30.0",
|
||||
@@ -7644,6 +7654,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"dev_server_projects",
|
||||
"env_logger",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
@@ -7673,6 +7684,7 @@ dependencies = [
|
||||
"sha2 0.10.7",
|
||||
"similar",
|
||||
"smol",
|
||||
"snippet",
|
||||
"task",
|
||||
"terminal",
|
||||
"text",
|
||||
@@ -8034,6 +8046,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"markdown",
|
||||
"menu",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
@@ -8041,9 +8054,7 @@ dependencies = [
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_text_field",
|
||||
"util",
|
||||
@@ -8131,9 +8142,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
@@ -8196,7 +8207,7 @@ version = "0.11.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bytes 1.5.0",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
@@ -8578,7 +8589,7 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
||||
dependencies = [
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9589,7 +9600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"byteorder",
|
||||
@@ -9636,7 +9647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"byteorder",
|
||||
@@ -9725,7 +9736,6 @@ name = "storybook"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant2",
|
||||
"clap 4.4.4",
|
||||
"collab_ui",
|
||||
"ctrlc",
|
||||
@@ -9774,6 +9784,12 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
@@ -10358,12 +10374,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.5.7"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb"
|
||||
checksum = "c314e7ce51440f9e8f5a497394682a57b7c323d0f4d0a6b1b13c429056e0e234"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.4",
|
||||
"base64 0.21.7",
|
||||
"bstr",
|
||||
"fancy-regex",
|
||||
"lazy_static",
|
||||
@@ -10409,6 +10425,15 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
@@ -10869,8 +10894,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-go"
|
||||
version = "0.19.1"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=aeb2f33b366fd78d5789ff104956ce23508b85db#aeb2f33b366fd78d5789ff104956ce23508b85db"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=b82ab803d887002a0af11f6ce63d72884580bf33#b82ab803d887002a0af11f6ce63d72884580bf33"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -12811,6 +12836,35 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xim"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"ahash 0.8.8",
|
||||
"hashbrown 0.14.0",
|
||||
"log",
|
||||
"x11rb",
|
||||
"xim-ctext",
|
||||
"xim-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xim-ctext"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xim-parser"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon"
|
||||
version = "0.7.0"
|
||||
@@ -12941,13 +12995,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.136.0"
|
||||
version = "0.137.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"assistant",
|
||||
"assistant2",
|
||||
"audio",
|
||||
"auto_update",
|
||||
"backtrace",
|
||||
@@ -12967,7 +13020,6 @@ dependencies = [
|
||||
"diagnostics",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"event_server",
|
||||
"extension",
|
||||
"extensions_ui",
|
||||
"feedback",
|
||||
@@ -13043,7 +13095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_astro"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13092,7 +13144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_emmet"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13135,7 +13187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_gleam"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -13156,9 +13208,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13177,7 +13229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13198,7 +13250,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruby"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -23,7 +23,6 @@ members = [
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
"crates/event_server",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -169,7 +168,6 @@ copilot = { path = "crates/copilot" }
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
event_server = { path = "crates/event_server" }
|
||||
extension = { path = "crates/extension" }
|
||||
extensions_ui = { path = "crates/extensions_ui" }
|
||||
feature_flags = { path = "crates/feature_flags" }
|
||||
@@ -339,7 +337,7 @@ subtle = "2.5.0"
|
||||
sysinfo = "0.30.7"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
tiktoken-rs = "0.5.7"
|
||||
tiktoken-rs = "0.5.9"
|
||||
time = { version = "0.3", features = [
|
||||
"macros",
|
||||
"parsing",
|
||||
@@ -357,7 +355,7 @@ tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev
|
||||
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-embedded-template = "0.20.0"
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "b82ab803d887002a0af11f6ce63d72884580bf33" }
|
||||
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
rustc-demangle = "0.1.23"
|
||||
@@ -404,11 +402,11 @@ features = [
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Imaging_D2D",
|
||||
"Win32_Media",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_Storage_FileSystem",
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
"gitkeep": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"go": "go",
|
||||
"gql": "graphql",
|
||||
"graphql": "graphql",
|
||||
"graphqls": "graphql",
|
||||
"h": "c",
|
||||
"hpp": "cpp",
|
||||
"handlebars": "code",
|
||||
|
||||
1
assets/icons/library.svg
Normal file
1
assets/icons/library.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-library"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>
|
||||
|
After Width: | Height: | Size: 298 B |
@@ -53,7 +53,9 @@
|
||||
// "alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
@@ -189,6 +191,12 @@
|
||||
"ctrl-shift-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Markdown",
|
||||
"bindings": {
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
@@ -546,7 +554,9 @@
|
||||
"alt-ctrl-n": "project_panel::NewDirectory",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-insert": "project_panel::Copy",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"shift-insert": "project_panel::Paste",
|
||||
"ctrl-alt-c": "project_panel::CopyPath",
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
@@ -608,7 +618,9 @@
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"shift-ctrl-c": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
"shift-ctrl-v": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
|
||||
@@ -208,11 +208,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantChat > Editor", // Used in the assistant2 crate
|
||||
"context": "Markdown",
|
||||
"bindings": {
|
||||
"enter": ["assistant2::Submit", "Simple"],
|
||||
"cmd-enter": ["assistant2::Submit", "Codebase"],
|
||||
"escape": "assistant2::Cancel"
|
||||
"cmd-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -573,7 +571,6 @@
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
{
|
||||
// The name of the Zed theme to use for the UI
|
||||
// The name of the Zed theme to use for the UI.
|
||||
//
|
||||
// The theme can also be set to follow system preferences:
|
||||
//
|
||||
// "theme": {
|
||||
// "mode": "system",
|
||||
// "light": "One Light",
|
||||
// "dark": "One Dark"
|
||||
// }
|
||||
//
|
||||
// Where `mode` is one of:
|
||||
// - "system": Use the theme that corresponds to the system's appearance
|
||||
// - "light": Use the theme indicated by the "light" field
|
||||
// - "dark": Use the theme indicated by the "dark" field
|
||||
"theme": "One Dark",
|
||||
// The name of a base set of key bindings to use.
|
||||
// This setting can take four values, each named after another
|
||||
@@ -71,6 +84,15 @@
|
||||
"restore_on_startup": "last_workspace",
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the window should be closed when using 'close active item' on a window with no tabs.
|
||||
// May take 3 values:
|
||||
// 1. Use the current platform's convention
|
||||
// "when_closing_with_no_tabs": "platform_default"
|
||||
// 2. Always close the window:
|
||||
// "when_closing_with_no_tabs": "close_window",
|
||||
// 3. Never close the window
|
||||
// "when_closing_with_no_tabs": "keep_window_open",
|
||||
"when_closing_with_no_tabs": "platform_default",
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// How to highlight the current line in the editor.
|
||||
@@ -311,9 +333,7 @@
|
||||
// The list of language servers to use (or disable) for all languages.
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
"language_servers": [
|
||||
"..."
|
||||
],
|
||||
"language_servers": ["..."],
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
@@ -448,9 +468,7 @@
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
"disabled_globs": [".env"]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -479,7 +497,7 @@
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
|
||||
// Where to dock terminals panel. Can be `left`, `right`, `bottom`.
|
||||
"dock": "bottom",
|
||||
// Default width when the terminal is docked to the left or right.
|
||||
"default_width": 640,
|
||||
@@ -561,13 +579,8 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
// Can also be `csh`, `fish`, and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
},
|
||||
@@ -592,7 +605,7 @@
|
||||
// use those languages.
|
||||
//
|
||||
// For example, to treat files like `foo.notjs` as JavaScript,
|
||||
// and 'Embargo.lock' as TOML:
|
||||
// and `Embargo.lock` as TOML:
|
||||
//
|
||||
// {
|
||||
// "JavaScript": ["notjs"],
|
||||
@@ -609,19 +622,30 @@
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
"Astro": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-astro"]
|
||||
}
|
||||
},
|
||||
"Blade": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"C": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"CSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Elixir": {
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
@@ -631,32 +655,120 @@
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"GraphQL": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"HEEX": {
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"HTML": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Java": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-java"]
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"JSON": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": "off",
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"PHP": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
}
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"SQL": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-sql"]
|
||||
}
|
||||
},
|
||||
"Svelte": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Twig": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Vue.js": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"XML": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-xml"]
|
||||
}
|
||||
},
|
||||
"YAML": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// project has no other Prettier installed.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
"prettier": {
|
||||
// Use regular Prettier json configuration:
|
||||
// // Whether to consider prettier formatter or not when attempting to format a file.
|
||||
// "allowed": false,
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// "plugins": [],
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// "trailingComma": "es5",
|
||||
// "tabWidth": 4,
|
||||
// "semi": false,
|
||||
@@ -714,5 +826,17 @@
|
||||
// - `short`: "2 s, 15 l, 32 c"
|
||||
// - `long`: "2 selections, 15 lines, 32 characters"
|
||||
// Default: long
|
||||
"line_indicator_format": "long"
|
||||
"line_indicator_format": "long",
|
||||
// Set a proxy to use. The proxy protocol is specified by the URI scheme.
|
||||
//
|
||||
// Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
|
||||
// `socks5h`. `http` will be used when no scheme is specified.
|
||||
//
|
||||
// By default no proxy will be used, or Zed will try get proxy settings from
|
||||
// environment variables.
|
||||
//
|
||||
// Examples:
|
||||
// - "proxy" = "socks5://localhost:10808"
|
||||
// - "proxy" = "http://127.0.0.1:10809"
|
||||
"proxy": null
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -32,15 +33,18 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strsim = "0.11"
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
@@ -53,3 +57,4 @@ env_logger.workspace = true
|
||||
log.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
||||
30
crates/assistant/src/ambient_context.rs
Normal file
30
crates/assistant/src/ambient_context.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
mod current_project;
|
||||
mod recent_buffers;
|
||||
|
||||
pub use current_project::*;
|
||||
pub use recent_buffers::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AmbientContext {
|
||||
pub recent_buffers: RecentBuffersContext,
|
||||
pub current_project: CurrentProjectContext,
|
||||
}
|
||||
|
||||
impl AmbientContext {
|
||||
pub fn snapshot(&self) -> AmbientContextSnapshot {
|
||||
AmbientContextSnapshot {
|
||||
recent_buffers: self.recent_buffers.snapshot.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct AmbientContextSnapshot {
|
||||
pub recent_buffers: RecentBuffersSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||
pub enum ContextUpdated {
|
||||
Updating,
|
||||
Disabled,
|
||||
}
|
||||
178
crates/assistant/src/ambient_context/current_project.rs
Normal file
178
crates/assistant/src/ambient_context/current_project.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
|
||||
use project::{Project, ProjectPath};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::ambient_context::ContextUpdated;
|
||||
use crate::assistant_panel::Conversation;
|
||||
use crate::{LanguageModelRequestMessage, Role};
|
||||
|
||||
/// Ambient context about the current project.
|
||||
pub struct CurrentProjectContext {
|
||||
pub enabled: bool,
|
||||
pub message: String,
|
||||
pub pending_message: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for CurrentProjectContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
message: String::new(),
|
||||
pending_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentProjectContext {
|
||||
/// Returns the [`CurrentProjectContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.message.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
|
||||
pub fn update(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
project: WeakModel<Project>,
|
||||
cx: &mut ModelContext<Conversation>,
|
||||
) -> ContextUpdated {
|
||||
if !self.enabled {
|
||||
self.message.clear();
|
||||
self.pending_message = None;
|
||||
cx.notify();
|
||||
return ContextUpdated::Disabled;
|
||||
}
|
||||
|
||||
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
|
||||
let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(path_to_cargo_toml) = path_to_cargo_toml
|
||||
.ok_or_else(|| anyhow!("no Cargo.toml"))
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let message_task = cx
|
||||
.background_executor()
|
||||
.spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
|
||||
|
||||
if let Some(message) = message_task.await.log_err() {
|
||||
conversation
|
||||
.update(&mut cx, |conversation, cx| {
|
||||
conversation.ambient_context.current_project.message = message;
|
||||
conversation.count_remaining_tokens(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
|
||||
ContextUpdated::Updating
|
||||
}
|
||||
|
||||
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
|
||||
let buffer = fs.load(path_to_cargo_toml).await?;
|
||||
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
|
||||
|
||||
let mut message = String::new();
|
||||
writeln!(message, "You are in a Rust project.")?;
|
||||
|
||||
if let Some(workspace) = cargo_toml.workspace {
|
||||
writeln!(
|
||||
message,
|
||||
"The project is a Cargo workspace with the following members:"
|
||||
)?;
|
||||
for member in workspace.members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
|
||||
if !workspace.default_members.is_empty() {
|
||||
writeln!(message, "The default members are:")?;
|
||||
for member in workspace.default_members {
|
||||
writeln!(message, "- {member}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if !workspace.dependencies.is_empty() {
|
||||
writeln!(
|
||||
message,
|
||||
"The following workspace dependencies are installed:"
|
||||
)?;
|
||||
for dependency in workspace.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
} else if let Some(package) = cargo_toml.package {
|
||||
writeln!(
|
||||
message,
|
||||
"The project name is \"{name}\".",
|
||||
name = package.name
|
||||
)?;
|
||||
|
||||
let description = package
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|description| description.get().ok().cloned());
|
||||
if let Some(description) = description.as_ref() {
|
||||
writeln!(message, "It describes itself as \"{description}\".")?;
|
||||
}
|
||||
|
||||
if !cargo_toml.dependencies.is_empty() {
|
||||
writeln!(message, "The following dependencies are installed:")?;
|
||||
for dependency in cargo_toml.dependencies.keys() {
|
||||
writeln!(message, "- {dependency}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(
|
||||
project: WeakModel<Project>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
cx.update(|cx| {
|
||||
let worktree = project.update(cx, |project, _cx| {
|
||||
project
|
||||
.worktrees()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("no worktree"))
|
||||
})??;
|
||||
|
||||
let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
|
||||
let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
|
||||
Some(ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: cargo_toml.path.clone(),
|
||||
})
|
||||
});
|
||||
let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
|
||||
project
|
||||
.update(cx, |project, cx| project.absolute_path(&path, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
Ok(path_to_cargo_toml)
|
||||
})?
|
||||
}
|
||||
}
|
||||
145
crates/assistant/src/ambient_context/recent_buffers.rs
Normal file
145
crates/assistant/src/ambient_context/recent_buffers.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
|
||||
use gpui::{ModelContext, Subscription, Task, WeakModel};
|
||||
use language::{Buffer, BufferSnapshot, Rope};
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
|
||||
use super::ContextUpdated;
|
||||
|
||||
pub struct RecentBuffersContext {
|
||||
pub enabled: bool,
|
||||
pub buffers: Vec<RecentBuffer>,
|
||||
pub snapshot: RecentBuffersSnapshot,
|
||||
pub pending_message: Option<Task<()>>,
|
||||
}
|
||||
|
||||
pub struct RecentBuffer {
|
||||
pub buffer: WeakModel<Buffer>,
|
||||
pub _subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Default for RecentBuffersContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
buffers: Vec::new(),
|
||||
snapshot: RecentBuffersSnapshot::default(),
|
||||
pending_message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RecentBuffersContext {
|
||||
pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
|
||||
let source_buffers = self
|
||||
.buffers
|
||||
.iter()
|
||||
.filter_map(|recent| {
|
||||
let (full_path, snapshot) = recent
|
||||
.buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
(
|
||||
buffer.file().map(|file| file.full_path(cx)),
|
||||
buffer.snapshot(),
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
Some(SourceBufferSnapshot {
|
||||
full_path,
|
||||
model: recent.buffer.clone(),
|
||||
snapshot,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !self.enabled || source_buffers.is_empty() {
|
||||
self.snapshot.message = Default::default();
|
||||
self.snapshot.source_buffers.clear();
|
||||
self.pending_message = None;
|
||||
cx.notify();
|
||||
ContextUpdated::Disabled
|
||||
} else {
|
||||
self.pending_message = Some(cx.spawn(|this, mut cx| async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
|
||||
let message = if source_buffers.is_empty() {
|
||||
Rope::new()
|
||||
} else {
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let source_buffers = source_buffers.clone();
|
||||
async move { message_for_recent_buffers(source_buffers) }
|
||||
})
|
||||
.await
|
||||
};
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
|
||||
this.ambient_context.recent_buffers.snapshot.message = message;
|
||||
this.count_remaining_tokens(cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
|
||||
ContextUpdated::Updating
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`RecentBuffersContext`] as a message to the language model.
|
||||
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
|
||||
self.enabled.then(|| LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: self.snapshot.message.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct RecentBuffersSnapshot {
|
||||
pub message: Rope,
|
||||
pub source_buffers: Vec<SourceBufferSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SourceBufferSnapshot {
|
||||
pub full_path: Option<PathBuf>,
|
||||
pub model: WeakModel<Buffer>,
|
||||
pub snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SourceBufferSnapshot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SourceBufferSnapshot")
|
||||
.field("full_path", &self.full_path)
|
||||
.field("model (entity id)", &self.model.entity_id())
|
||||
.field("snapshot (text)", &self.snapshot.text())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
|
||||
let mut message = String::new();
|
||||
writeln!(
|
||||
message,
|
||||
"The following is a list of recent buffers that the user has opened."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for buffer in buffers {
|
||||
if let Some(path) = buffer.full_path {
|
||||
writeln!(message, "```{}", path.display()).unwrap();
|
||||
} else {
|
||||
writeln!(message, "```untitled").unwrap();
|
||||
}
|
||||
|
||||
for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
|
||||
message.push_str(chunk.text);
|
||||
}
|
||||
if !message.ends_with('\n') {
|
||||
message.push('\n');
|
||||
}
|
||||
message.push_str("```\n");
|
||||
}
|
||||
|
||||
Rope::from(message.as_str())
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
mod ambient_context;
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod streaming_diff;
|
||||
|
||||
use ambient_context::AmbientContextSnapshot;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use saved_conversation::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -30,8 +34,10 @@ actions!(
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
InsertActivePrompt,
|
||||
ToggleIncludeConversation,
|
||||
ToggleHistory,
|
||||
ApplyEdit
|
||||
]
|
||||
);
|
||||
|
||||
@@ -181,6 +187,9 @@ pub struct LanguageModelChoiceDelta {
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
status: MessageStatus,
|
||||
// todo!("delete this")
|
||||
#[serde(skip)]
|
||||
ambient_context: AmbientContextSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -232,13 +241,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
});
|
||||
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -418,7 +418,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext, BorrowAppContext};
|
||||
use gpui::{AppContext, UpdateGlobal};
|
||||
use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
@@ -440,7 +440,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Ensure backward-compatibility.
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
@@ -460,7 +460,7 @@ mod tests {
|
||||
low_speed_timeout_in_seconds: None,
|
||||
}
|
||||
);
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
@@ -482,7 +482,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// The new version supports setting a custom model when using zed.dev.
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(
|
||||
r#"{
|
||||
|
||||
@@ -204,9 +204,7 @@ pub fn count_open_ai_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match request.model {
|
||||
LanguageModel::OpenAi(OpenAiModel::FourOmni)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
|
||||
| LanguageModel::Anthropic(_)
|
||||
LanguageModel::Anthropic(_)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
|
||||
|
||||
454
crates/assistant/src/prompt_library.rs
Normal file
454
crates/assistant/src/prompt_library.rs
Normal file
@@ -0,0 +1,454 @@
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Checkbox, ModalHeader};
|
||||
use util::{paths::PROMPTS_DIR, ResultExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct PromptLibraryState {
|
||||
/// The default prompt all assistant contexts will start with
|
||||
_system_prompt: String,
|
||||
/// All [UserPrompt]s loaded into the library
|
||||
prompts: HashMap<String, UserPrompt>,
|
||||
/// Prompts included in the default prompt
|
||||
default_prompts: Vec<String>,
|
||||
/// Prompts that have a pending update that hasn't been applied yet
|
||||
_updateable_prompts: Vec<String>,
|
||||
/// Prompts that have been changed since they were loaded
|
||||
/// and can be reverted to their original state
|
||||
_revertable_prompts: Vec<String>,
|
||||
version: usize,
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
state: RwLock<PromptLibraryState>,
|
||||
}
|
||||
|
||||
impl Default for PromptLibrary {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: RwLock::new(PromptLibraryState {
|
||||
_system_prompt: String::new(),
|
||||
prompts: HashMap::new(),
|
||||
default_prompts: Vec::new(),
|
||||
_updateable_prompts: Vec::new(),
|
||||
_revertable_prompts: Vec::new(),
|
||||
version: 0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
|
||||
let prompt_library = PromptLibrary::new();
|
||||
prompt_library.load_prompts(fs)?;
|
||||
Ok(prompt_library)
|
||||
}
|
||||
|
||||
fn load_prompts(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
let prompts = futures::executor::block_on(UserPrompt::list(fs))?;
|
||||
let prompts_with_ids = prompts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|prompt| {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
(id, prompt)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut state = self.state.write();
|
||||
state.prompts.extend(prompts_with_ids);
|
||||
state.version += 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn default_prompt(&self) -> Option<String> {
|
||||
let state = self.state.read();
|
||||
|
||||
if state.default_prompts.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.join_default_prompts())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
|
||||
if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) {
|
||||
state.default_prompts.push(prompt_id);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> {
|
||||
let mut state = self.state.write();
|
||||
|
||||
state.default_prompts.retain(|id| id != &prompt_id);
|
||||
state.version += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn join_default_prompts(&self) -> String {
|
||||
let state = self.state.read();
|
||||
let active_prompt_ids = state.default_prompts.to_vec();
|
||||
|
||||
active_prompt_ids
|
||||
.iter()
|
||||
.filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n---\n\n")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn prompts(&self) -> Vec<UserPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> {
|
||||
let state = self.state.read();
|
||||
state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (id.clone(), prompt.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn _default_prompts(&self) -> Vec<UserPrompt> {
|
||||
let state = self.state.read();
|
||||
state
|
||||
.default_prompts
|
||||
.iter()
|
||||
.filter_map(|id| state.prompts.get(id).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn default_prompt_ids(&self) -> Vec<String> {
|
||||
let state = self.state.read();
|
||||
state.default_prompts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom prompt that can be loaded into the prompt library
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "title": "Foo",
|
||||
/// "version": "1.0",
|
||||
/// "author": "Jane Kim <jane@kim.com>",
|
||||
/// "languages": ["*"], // or ["rust", "python", "javascript"] etc...
|
||||
/// "prompt": "bar"
|
||||
/// }
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct UserPrompt {
|
||||
version: String,
|
||||
title: String,
|
||||
author: String,
|
||||
languages: Vec<String>,
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
impl UserPrompt {
|
||||
async fn list(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<Self>> {
|
||||
fs.create_dir(&PROMPTS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&PROMPTS_DIR).await?;
|
||||
let mut prompts = Vec::new();
|
||||
|
||||
while let Some(path_result) = paths.next().await {
|
||||
let path = match path_result {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading path: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if path.extension() == Some(std::ffi::OsStr::new("json")) {
|
||||
match fs.load(&path).await {
|
||||
Ok(content) => {
|
||||
let user_prompt: UserPrompt =
|
||||
serde_json::from_str(&content).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e)
|
||||
})?;
|
||||
|
||||
prompts.push(user_prompt);
|
||||
}
|
||||
Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(prompts)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromptManager {
|
||||
focus_handle: FocusHandle,
|
||||
prompt_library: Arc<PromptLibrary>,
|
||||
active_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl PromptManager {
|
||||
pub fn new(prompt_library: Arc<PromptLibrary>, cx: &mut WindowContext) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
active_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_prompt(&mut self, prompt_id: Option<String>) {
|
||||
self.active_prompt = prompt_id;
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let prompt_library = self.prompt_library.clone();
|
||||
let prompts = prompt_library
|
||||
.clone()
|
||||
.prompts_with_ids()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let active_prompt = self.active_prompt.as_ref().and_then(|id| {
|
||||
prompt_library
|
||||
.prompts_with_ids()
|
||||
.iter()
|
||||
.find(|(prompt_id, _)| prompt_id == id)
|
||||
.map(|(_, prompt)| prompt.clone())
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptManager")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.elevation_3(cx)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.w(rems(54.))
|
||||
.h(rems(40.))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
ModalHeader::new("prompt-manager-header")
|
||||
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
|
||||
.show_dismiss_button(true),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_grow()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
div()
|
||||
.id("prompt-preview")
|
||||
.overflow_y_scroll()
|
||||
.h_full()
|
||||
.min_w_64()
|
||||
.max_w_1_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_start()
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.when_else(
|
||||
!prompts.is_empty(),
|
||||
|with_items| {
|
||||
with_items.children(prompts.into_iter().map(
|
||||
|(id, prompt)| {
|
||||
let prompt_library = prompt_library.clone();
|
||||
let prompt = prompt.clone();
|
||||
let prompt_id = id.clone();
|
||||
let shared_string_id: SharedString =
|
||||
id.clone().into();
|
||||
|
||||
let default_prompt_ids =
|
||||
prompt_library.clone().default_prompt_ids();
|
||||
let is_default =
|
||||
default_prompt_ids.contains(&id);
|
||||
// We'll use this for conditionally enabled prompts
|
||||
// like those loaded only for certain languages
|
||||
let is_conditional = false;
|
||||
let selection =
|
||||
match (is_default, is_conditional) {
|
||||
(_, true) => Selection::Indeterminate,
|
||||
(true, _) => Selection::Selected,
|
||||
(false, _) => Selection::Unselected,
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(ElementId::Name(
|
||||
format!("prompt-{}", shared_string_id)
|
||||
.into(),
|
||||
))
|
||||
.p(Spacing::Small.rems(cx))
|
||||
|
||||
.on_click(cx.listener({
|
||||
let prompt_id = prompt_id.clone();
|
||||
move |this, _event, _cx| {
|
||||
this.set_active_prompt(Some(
|
||||
prompt_id.clone(),
|
||||
));
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
shared_string_id,
|
||||
selection,
|
||||
)
|
||||
.on_click(move |_, _cx| {
|
||||
if is_default {
|
||||
prompt_library
|
||||
.clone()
|
||||
.remove_prompt_from_default(
|
||||
prompt_id.clone(),
|
||||
)
|
||||
.log_err();
|
||||
} else {
|
||||
prompt_library
|
||||
.clone()
|
||||
.add_prompt_to_default(
|
||||
prompt_id.clone(),
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(Label::new(
|
||||
prompt.title,
|
||||
)),
|
||||
)
|
||||
.child(div()),
|
||||
)
|
||||
},
|
||||
))
|
||||
},
|
||||
|no_items| {
|
||||
no_items.child(
|
||||
Label::new("No prompts").color(Color::Placeholder),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("prompt-preview")
|
||||
.overflow_y_scroll()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_start()
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.when_else(
|
||||
active_prompt.is_some(),
|
||||
|with_prompt| {
|
||||
let active_prompt = active_prompt.as_ref().unwrap();
|
||||
with_prompt
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Headline::new(
|
||||
active_prompt.title.clone(),
|
||||
)
|
||||
.size(HeadlineSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Label::new(
|
||||
active_prompt
|
||||
.author
|
||||
.clone(),
|
||||
)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
if active_prompt
|
||||
.languages
|
||||
.is_empty()
|
||||
|| active_prompt
|
||||
.languages[0]
|
||||
== "*"
|
||||
{
|
||||
" · Global".to_string()
|
||||
} else {
|
||||
format!(
|
||||
" · {}",
|
||||
active_prompt
|
||||
.languages
|
||||
.join(", ")
|
||||
)
|
||||
},
|
||||
)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.max_w(rems(30.))
|
||||
.text_ui(cx)
|
||||
.child(active_prompt.prompt.clone()),
|
||||
)
|
||||
},
|
||||
|without_prompt| {
|
||||
without_prompt.justify_center().items_center().child(
|
||||
Label::new("Select a prompt to view details.")
|
||||
.color(Color::Placeholder),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PromptManager {}
|
||||
impl ModalView for PromptManager {}
|
||||
|
||||
impl FocusableView for PromptManager {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
171
crates/assistant/src/search.rs
Normal file
171
crates/assistant/src/search.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use language::Rope;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Search the given buffer for the given substring, ignoring any differences
|
||||
/// in line indentation between the query and the buffer.
|
||||
///
|
||||
/// Returns a vector of ranges of byte offsets in the buffer corresponding
|
||||
/// to the entire lines of the buffer.
|
||||
pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option<Range<usize>> {
|
||||
const SIMILARITY_THRESHOLD: f64 = 0.8;
|
||||
|
||||
let mut best_match: Option<(Range<usize>, f64)> = None; // (range, score)
|
||||
let mut haystack_lines = haystack.chunks().lines();
|
||||
let mut haystack_line_start = 0;
|
||||
while let Some(mut haystack_line) = haystack_lines.next() {
|
||||
let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1;
|
||||
let mut advanced_to_next_haystack_line = false;
|
||||
|
||||
let mut matched = true;
|
||||
let match_start = haystack_line_start;
|
||||
let mut match_end = next_haystack_line_start;
|
||||
let mut match_score = 0.0;
|
||||
let mut needle_lines = needle.lines().peekable();
|
||||
while let Some(needle_line) = needle_lines.next() {
|
||||
let similarity = line_similarity(haystack_line, needle_line);
|
||||
if similarity >= SIMILARITY_THRESHOLD {
|
||||
match_end = haystack_lines.offset();
|
||||
match_score += similarity;
|
||||
|
||||
if needle_lines.peek().is_some() {
|
||||
if let Some(next_haystack_line) = haystack_lines.next() {
|
||||
advanced_to_next_haystack_line = true;
|
||||
haystack_line = next_haystack_line;
|
||||
} else {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if matched
|
||||
&& best_match
|
||||
.as_ref()
|
||||
.map(|(_, best_score)| match_score > *best_score)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
best_match = Some((match_start..match_end, match_score));
|
||||
}
|
||||
|
||||
if advanced_to_next_haystack_line {
|
||||
haystack_lines.seek(next_haystack_line_start);
|
||||
}
|
||||
haystack_line_start = next_haystack_line_start;
|
||||
}
|
||||
|
||||
best_match.map(|(range, _)| range)
|
||||
}
|
||||
|
||||
/// Calculates the similarity between two lines, ignoring leading and trailing whitespace,
|
||||
/// using the Jaro-Winkler distance.
|
||||
///
|
||||
/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match.
|
||||
fn line_similarity(line1: &str, line2: &str) -> f64 {
|
||||
strsim::jaro_winkler(line1.trim(), line2.trim())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::{AppContext, Context as _};
|
||||
use language::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_fuzzy_search_lines(cx: &mut AppContext) {
|
||||
let (text, expected_ranges) = marked_text_ranges(
|
||||
&r#"
|
||||
fn main() {
|
||||
if a() {
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
does_not_match,
|
||||
);
|
||||
}
|
||||
|
||||
println!("hi");
|
||||
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
); // this last line does not match
|
||||
|
||||
« assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
»
|
||||
|
||||
« assert_eq!(
|
||||
"something",
|
||||
"else",
|
||||
);
|
||||
»
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer = cx.new_model(|cx| Buffer::local(&text, cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[0]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
1 + 2,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[0]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
asst_eq!(
|
||||
\"something\",
|
||||
\"els\"
|
||||
)
|
||||
"
|
||||
.unindent(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(actual_range, expected_ranges[1]);
|
||||
|
||||
let actual_range = fuzzy_search_lines(
|
||||
snapshot.as_rope(),
|
||||
&"
|
||||
assert_eq!(
|
||||
2 + 1,
|
||||
3,
|
||||
);
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
assert_eq!(actual_range, None);
|
||||
}
|
||||
}
|
||||
86
crates/assistant/src/system_prompts/edits.md
Normal file
86
crates/assistant/src/system_prompts/edits.md
Normal file
@@ -0,0 +1,86 @@
|
||||
When the user asks you to suggest edits for a buffer, use a strict template consisting of:
|
||||
|
||||
* A markdown code block with the file path as the language identifier.
|
||||
* The original code that should be replaced
|
||||
* A separator line (`---`)
|
||||
* The new text that should replace the original lines
|
||||
|
||||
Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
|
||||
|
||||
## Example
|
||||
|
||||
If you have a buffer with the following lines:
|
||||
|
||||
```path/to/file.rs
|
||||
fn quicksort(arr: &mut [i32]) {
|
||||
if arr.len() <= 1 {
|
||||
return;
|
||||
}
|
||||
let pivot_index = partition(arr);
|
||||
let (left, right) = arr.split_at_mut(pivot_index);
|
||||
quicksort(left);
|
||||
quicksort(&mut right[1..]);
|
||||
}
|
||||
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
let last_index = arr.len() - 1;
|
||||
let pivot = arr[last_index];
|
||||
let mut i = 0;
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
arr.swap(i, last_index);
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
And you want to replace the for loop inside `partition`, output the following.
|
||||
|
||||
```edit path/to/file.rs
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
---
|
||||
let mut j = 0;
|
||||
while j < last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
```
|
||||
|
||||
If you wanted to insert comments above the partition function, output the following:
|
||||
|
||||
```edit path/to/file.rs
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
---
|
||||
// A helper function used for quicksort.
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
```
|
||||
|
||||
If you wanted to delete the partition function, output the following:
|
||||
|
||||
```edit path/to/file.rs
|
||||
fn partition(arr: &mut [i32]) -> usize {
|
||||
let last_index = arr.len() - 1;
|
||||
let pivot = arr[last_index];
|
||||
let mut i = 0;
|
||||
for j in 0..last_index {
|
||||
if arr[j] <= pivot {
|
||||
arr.swap(i, j);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
arr.swap(i, last_index);
|
||||
i
|
||||
}
|
||||
---
|
||||
```
|
||||
@@ -24,7 +24,8 @@ use fs::Fs;
|
||||
use futures::{future::join_all, StreamExt};
|
||||
use gpui::{
|
||||
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
|
||||
FocusableView, ListAlignment, ListState, Model, ReadGlobal, Render, Task, UpdateGlobal, View,
|
||||
WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
@@ -124,7 +125,7 @@ impl AssistantPanel {
|
||||
})?;
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
|
||||
let project_index = SemanticIndex::update_global(cx, |semantic_index, cx| {
|
||||
semantic_index.project_index(project.clone(), cx)
|
||||
});
|
||||
|
||||
@@ -288,7 +289,7 @@ impl AssistantChat {
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model = CompletionProvider::get(cx).default_model();
|
||||
let model = CompletionProvider::global(cx).default_model();
|
||||
let view = cx.view().downgrade();
|
||||
let list_state = ListState::new(
|
||||
0,
|
||||
@@ -439,7 +440,7 @@ impl AssistantChat {
|
||||
Markdown::new(
|
||||
text,
|
||||
self.markdown_style.clone(),
|
||||
self.language_registry.clone(),
|
||||
Some(self.language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -550,7 +551,7 @@ impl AssistantChat {
|
||||
let messages = messages.await?;
|
||||
|
||||
let completion = cx.update(|cx| {
|
||||
CompletionProvider::get(cx).complete(
|
||||
CompletionProvider::global(cx).complete(
|
||||
model_name,
|
||||
messages,
|
||||
Vec::new(),
|
||||
@@ -572,7 +573,7 @@ impl AssistantChat {
|
||||
Markdown::new(
|
||||
"".into(),
|
||||
this.markdown_style.clone(),
|
||||
this.language_registry.clone(),
|
||||
Some(this.language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -666,7 +667,7 @@ impl AssistantChat {
|
||||
Markdown::new(
|
||||
"".into(),
|
||||
self.markdown_style.clone(),
|
||||
self.language_registry.clone(),
|
||||
Some(self.language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -682,7 +683,7 @@ impl AssistantChat {
|
||||
Markdown::new(
|
||||
"".into(),
|
||||
self.markdown_style.clone(),
|
||||
self.language_registry.clone(),
|
||||
Some(self.language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use assistant_tooling::ToolFunctionDefinition;
|
||||
use client::{proto, Client};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, Global};
|
||||
use gpui::Global;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use open_ai::RequestMessage as CompletionMessage;
|
||||
@@ -11,10 +11,6 @@ pub use open_ai::RequestMessage as CompletionMessage;
|
||||
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
|
||||
|
||||
impl CompletionProvider {
|
||||
pub fn get(cx: &AppContext) -> &Self {
|
||||
cx.global::<CompletionProvider>()
|
||||
}
|
||||
|
||||
pub fn new(backend: impl CompletionProviderBackend) -> Self {
|
||||
Self(Arc::new(backend))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
AssistantChat, CompletionProvider,
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
|
||||
use gpui::{AnyElement, FontStyle, FontWeight, ReadGlobal, TextStyle, View, WeakView, WhiteSpace};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
|
||||
@@ -139,7 +139,7 @@ impl RenderOnce for ModelSelector {
|
||||
popover_menu("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::get(cx).available_models() {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
|
||||
@@ -123,6 +123,7 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
@@ -815,9 +816,11 @@ impl ChannelStore {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
pub fn get_channel_member_details(
|
||||
pub fn fuzzy_search_members(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
query: String,
|
||||
limit: u16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<ChannelMembership>>> {
|
||||
let client = self.client.clone();
|
||||
@@ -826,26 +829,24 @@ impl ChannelStore {
|
||||
let response = client
|
||||
.request(proto::GetChannelMembers {
|
||||
channel_id: channel_id.0,
|
||||
query,
|
||||
limit: limit as u64,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let user_ids = response.members.iter().map(|m| m.user_id).collect();
|
||||
let user_store = user_store
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("user store dropped"))?;
|
||||
let users = user_store
|
||||
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))?
|
||||
.await?;
|
||||
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.zip(response.members)
|
||||
.map(|(user, member)| ChannelMembership {
|
||||
user,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
.collect())
|
||||
user_store.update(&mut cx, |user_store, _| {
|
||||
user_store.insert(response.users);
|
||||
response
|
||||
.members
|
||||
.into_iter()
|
||||
.filter_map(|member| {
|
||||
Some(ChannelMembership {
|
||||
user: user_store.get_cached_user(member.user_id)?,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
|
||||
use std::{
|
||||
env, fs,
|
||||
env, fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitStatus,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
@@ -14,6 +16,7 @@ struct Detect;
|
||||
trait InstalledApp {
|
||||
fn zed_version_string(&self) -> String;
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
|
||||
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -37,6 +40,9 @@ struct Args {
|
||||
/// Print Zed's version and the app path.
|
||||
#[arg(short, long)]
|
||||
version: bool,
|
||||
/// Run zed in the foreground (useful for debugging)
|
||||
#[arg(long)]
|
||||
foreground: bool,
|
||||
/// Custom path to Zed.app or the zed binary
|
||||
#[arg(long)]
|
||||
zed: Option<PathBuf>,
|
||||
@@ -99,10 +105,6 @@ fn main() -> Result<()> {
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
app.launch(url)?;
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
let open_new_workspace = if args.new {
|
||||
Some(true)
|
||||
} else if args.add {
|
||||
@@ -111,20 +113,33 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
dev_server_token: args.dev_server_token,
|
||||
})?;
|
||||
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
CliResponse::Ping => {}
|
||||
CliResponse::Stdout { message } => println!("{message}"),
|
||||
CliResponse::Stderr { message } => eprintln!("{message}"),
|
||||
CliResponse::Exit { status } => std::process::exit(status),
|
||||
while let Ok(response) = rx.recv() {
|
||||
match response {
|
||||
CliResponse::Ping => {}
|
||||
CliResponse::Stdout { message } => println!("{message}"),
|
||||
CliResponse::Stderr { message } => eprintln!("{message}"),
|
||||
CliResponse::Exit { status } => std::process::exit(status),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if args.foreground {
|
||||
app.run_foreground(url)?;
|
||||
} else {
|
||||
app.launch(url)?;
|
||||
sender.join().unwrap()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -141,7 +156,8 @@ mod linux {
|
||||
unix::net::{SocketAddr, UnixDatagram},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
process, thread,
|
||||
process::{self, ExitStatus},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -208,6 +224,12 @@ mod linux {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
|
||||
std::process::Command::new(self.0.clone())
|
||||
.arg(ipc_url)
|
||||
.status()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -257,7 +279,9 @@ mod linux {
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
struct App;
|
||||
impl InstalledApp for App {
|
||||
@@ -267,6 +291,9 @@ mod windows {
|
||||
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Detect {
|
||||
@@ -288,9 +315,9 @@ mod mac_os {
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
process::{Command, ExitStatus},
|
||||
ptr,
|
||||
};
|
||||
|
||||
@@ -442,6 +469,15 @@ mod mac_os {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
|
||||
let path = match self {
|
||||
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
|
||||
Bundle::LocalPath { executable, .. } => executable.clone(),
|
||||
};
|
||||
|
||||
std::process::Command::new(path).arg(ipc_url).status()
|
||||
}
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
|
||||
@@ -17,8 +17,7 @@ use futures::{
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, BorrowAppContext, Global, Model,
|
||||
Task, WeakModel,
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -29,7 +28,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
@@ -86,7 +85,7 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
@@ -114,11 +113,35 @@ impl Settings for ClientSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProxySettingsContent {
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ProxySettings {
|
||||
pub proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for ProxySettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = ProxySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
Ok(Self {
|
||||
proxy: sources
|
||||
.user
|
||||
.and_then(|value| value.proxy.clone())
|
||||
.or(sources.default.proxy.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
TelemetrySettings::register(cx);
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.register_setting::<ClientSettings>(cx);
|
||||
});
|
||||
ClientSettings::register(cx);
|
||||
ProxySettings::register(cx);
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
@@ -512,6 +535,7 @@ impl Client {
|
||||
let clock = Arc::new(clock::RealSystemClock);
|
||||
let http = Arc::new(HttpClientWithUrl::new(
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
ProxySettings::get_global(cx).proxy.clone(),
|
||||
));
|
||||
Self::new(clock, http.clone(), cx)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ use std::io::Write;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
|
||||
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent,
|
||||
SettingEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
|
||||
MemoryEvent, SettingEvent,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -241,14 +241,14 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
pub fn report_inline_completion_event(
|
||||
self: &Arc<Self>,
|
||||
suggestion_id: Option<String>,
|
||||
provider: String,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
) {
|
||||
let event = Event::Copilot(CopilotEvent {
|
||||
suggestion_id,
|
||||
let event = Event::InlineCompletion(InlineCompletionEvent {
|
||||
provider,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ pub enum ContactRequestStatus {
|
||||
|
||||
pub struct UserStore {
|
||||
users: HashMap<u64, Arc<User>>,
|
||||
by_github_login: HashMap<String, u64>,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
@@ -144,6 +145,7 @@ impl UserStore {
|
||||
];
|
||||
Self {
|
||||
users: Default::default(),
|
||||
by_github_login: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
contacts: Default::default(),
|
||||
incoming_contact_requests: Default::default(),
|
||||
@@ -231,6 +233,7 @@ impl UserStore {
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.users.clear();
|
||||
self.by_github_login.clear();
|
||||
}
|
||||
|
||||
async fn handle_update_invite_info(
|
||||
@@ -644,6 +647,12 @@ impl UserStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cached_user_by_github_login(&self, github_login: &str) -> Option<Arc<User>> {
|
||||
self.by_github_login
|
||||
.get(github_login)
|
||||
.and_then(|id| self.users.get(id).cloned())
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> Option<Arc<User>> {
|
||||
self.current_user.borrow().clone()
|
||||
}
|
||||
@@ -661,26 +670,31 @@ impl UserStore {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(rpc) = client.upgrade() {
|
||||
let response = rpc.request(request).await.context("error loading users")?;
|
||||
let users = response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(User::new)
|
||||
.collect::<Vec<_>>();
|
||||
let users = response.users;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
for user in &users {
|
||||
this.users.insert(user.id, user.clone());
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(users)
|
||||
this.update(&mut cx, |this, _| this.insert(users))
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, users: Vec<proto::User>) -> Vec<Arc<User>> {
|
||||
let mut ret = Vec::with_capacity(users.len());
|
||||
for user in users {
|
||||
let user = User::new(user);
|
||||
if let Some(old) = self.users.insert(user.id, user.clone()) {
|
||||
if old.github_login != user.github_login {
|
||||
self.by_github_login.remove(&old.github_login);
|
||||
}
|
||||
}
|
||||
self.by_github_login
|
||||
.insert(user.github_login.clone(), user.id);
|
||||
ret.push(user)
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn set_participant_indices(
|
||||
&mut self,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
|
||||
@@ -407,6 +407,7 @@ CREATE TABLE dev_servers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
ssh_connection_string TEXT,
|
||||
hashed_token TEXT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE dev_servers ADD COLUMN ssh_connection_string TEXT;
|
||||
@@ -15,8 +15,9 @@ use serde::{Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
|
||||
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent,
|
||||
SettingEvent,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -26,6 +27,7 @@ pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/telemetry/events", post(post_events))
|
||||
.route("/telemetry/crashes", post(post_crash))
|
||||
.route("/telemetry/panics", post(post_panic))
|
||||
.route("/telemetry/hangs", post(post_hang))
|
||||
}
|
||||
|
||||
@@ -280,30 +282,77 @@ pub async fn post_hang(
|
||||
backtrace = %backtrace,
|
||||
"hang report");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn post_panic(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
|
||||
body: Bytes,
|
||||
) -> Result<()> {
|
||||
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
|
||||
return Err(Error::Http(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"events not enabled".into(),
|
||||
))?;
|
||||
};
|
||||
|
||||
if checksum != expected {
|
||||
return Err(Error::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid checksum".into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
|
||||
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let panic = report.panic;
|
||||
|
||||
tracing::error!(
|
||||
service = "client",
|
||||
version = %panic.app_version,
|
||||
os_name = %panic.os_name,
|
||||
os_version = %panic.os_version.clone().unwrap_or_default(),
|
||||
installation_id = %panic.installation_id.unwrap_or_default(),
|
||||
description = %panic.payload,
|
||||
backtrace = %panic.backtrace.join("\n"),
|
||||
"panic report");
|
||||
|
||||
let backtrace = if panic.backtrace.len() > 25 {
|
||||
let total = panic.backtrace.len();
|
||||
format!(
|
||||
"{}\n and {} more",
|
||||
panic
|
||||
.backtrace
|
||||
.iter()
|
||||
.take(20)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
total - 20
|
||||
)
|
||||
} else {
|
||||
panic.backtrace.join("\n")
|
||||
};
|
||||
let backtrace_with_summary = panic.payload + "\n" + &backtrace;
|
||||
|
||||
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
|
||||
let payload = slack::WebhookBody::new(|w| {
|
||||
w.add_section(|s| s.text(slack::Text::markdown("Possible Hang".to_string())))
|
||||
w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
|
||||
.add_section(|s| {
|
||||
s.add_field(slack::Text::markdown(format!(
|
||||
"*Version:*\n {} ",
|
||||
report.app_version.unwrap_or_default()
|
||||
panic.app_version
|
||||
)))
|
||||
.add_field({
|
||||
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
|
||||
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
|
||||
hostname.strip_prefix("http://").unwrap_or_default()
|
||||
});
|
||||
|
||||
slack::Text::markdown(format!(
|
||||
"*Incident:*\n<https://{}.{}/{}.hang.json|{}…>",
|
||||
CRASH_REPORTS_BUCKET,
|
||||
hostname,
|
||||
incident_id,
|
||||
incident_id.chars().take(8).collect::<String>(),
|
||||
"*OS:*\n{} {}",
|
||||
panic.os_name,
|
||||
panic.os_version.unwrap_or_default()
|
||||
))
|
||||
})
|
||||
})
|
||||
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace)))
|
||||
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
|
||||
});
|
||||
let payload_json = serde_json::to_string(&payload).map_err(|err| {
|
||||
log::error!("Failed to serialize payload to JSON: {err}");
|
||||
@@ -376,13 +425,19 @@ pub async fn post_events(
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
)),
|
||||
Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
)),
|
||||
// Needed for clients sending old copilot_event types
|
||||
Event::Copilot(_) => {}
|
||||
Event::InlineCompletion(event) => {
|
||||
to_upload
|
||||
.inline_completion_events
|
||||
.push(InlineCompletionEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
country_code.clone(),
|
||||
))
|
||||
}
|
||||
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
event.clone(),
|
||||
&wrapper,
|
||||
@@ -464,7 +519,7 @@ pub async fn post_events(
|
||||
#[derive(Default)]
|
||||
struct ToUpload {
|
||||
editor_events: Vec<EditorEventRow>,
|
||||
copilot_events: Vec<CopilotEventRow>,
|
||||
inline_completion_events: Vec<InlineCompletionEventRow>,
|
||||
assistant_events: Vec<AssistantEventRow>,
|
||||
call_events: Vec<CallEventRow>,
|
||||
cpu_events: Vec<CpuEventRow>,
|
||||
@@ -483,14 +538,14 @@ impl ToUpload {
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
|
||||
|
||||
const COPILOT_EVENTS_TABLE: &str = "copilot_events";
|
||||
const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
|
||||
Self::upload_to_table(
|
||||
COPILOT_EVENTS_TABLE,
|
||||
&self.copilot_events,
|
||||
INLINE_COMPLETION_EVENTS_TABLE,
|
||||
&self.inline_completion_events,
|
||||
clickhouse_client,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to upload to table '{COPILOT_EVENTS_TABLE}'"))?;
|
||||
.with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
|
||||
|
||||
const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
|
||||
Self::upload_to_table(
|
||||
@@ -590,7 +645,7 @@ where
|
||||
|
||||
let country_code = country_code.as_bytes();
|
||||
|
||||
serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16)
|
||||
serializer.serialize_u16(((country_code[1] as u16) << 8) + country_code[0] as u16)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
@@ -660,9 +715,9 @@ impl EditorEventRow {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct CopilotEventRow {
|
||||
pub struct InlineCompletionEventRow {
|
||||
pub installation_id: String,
|
||||
pub suggestion_id: String,
|
||||
pub provider: String,
|
||||
pub suggestion_accepted: bool,
|
||||
pub app_version: String,
|
||||
pub file_extension: String,
|
||||
@@ -682,9 +737,9 @@ pub struct CopilotEventRow {
|
||||
pub patch: Option<i32>,
|
||||
}
|
||||
|
||||
impl CopilotEventRow {
|
||||
impl InlineCompletionEventRow {
|
||||
fn from_event(
|
||||
event: CopilotEvent,
|
||||
event: InlineCompletionEvent,
|
||||
wrapper: &EventWrapper,
|
||||
body: &EventRequestBody,
|
||||
first_event_at: chrono::DateTime<chrono::Utc>,
|
||||
@@ -711,7 +766,7 @@ impl CopilotEventRow {
|
||||
country_code: country_code.unwrap_or("XX".to_string()),
|
||||
region_code: "".to_string(),
|
||||
city: "".to_string(),
|
||||
suggestion_id: event.suggestion_id.unwrap_or_default(),
|
||||
provider: event.provider,
|
||||
suggestion_accepted: event.suggestion_accepted,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,8 +509,7 @@ pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
|
||||
|
||||
pub struct CreatedChannelMessage {
|
||||
pub message_id: MessageId,
|
||||
pub participant_connection_ids: Vec<ConnectionId>,
|
||||
pub channel_members: Vec<UserId>,
|
||||
pub participant_connection_ids: HashSet<ConnectionId>,
|
||||
pub notifications: NotificationBatch,
|
||||
}
|
||||
|
||||
|
||||
@@ -440,12 +440,7 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
user: UserId,
|
||||
operations: &[proto::Operation],
|
||||
) -> Result<(
|
||||
Vec<ConnectionId>,
|
||||
Vec<UserId>,
|
||||
i32,
|
||||
Vec<proto::VectorClockEntry>,
|
||||
)> {
|
||||
) -> Result<(HashSet<ConnectionId>, i32, Vec<proto::VectorClockEntry>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
|
||||
@@ -479,7 +474,6 @@ impl Database {
|
||||
.filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut channel_members;
|
||||
let max_version;
|
||||
|
||||
if !operations.is_empty() {
|
||||
@@ -504,12 +498,6 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_participants(&channel, &tx).await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !collaborators.contains(member));
|
||||
|
||||
buffer_operation::Entity::insert_many(operations)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -524,11 +512,10 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
channel_members = Vec::new();
|
||||
max_version = Vec::new();
|
||||
}
|
||||
|
||||
let mut connections = Vec::new();
|
||||
let mut connections = HashSet::default();
|
||||
let mut rows = channel_buffer_collaborator::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -538,13 +525,13 @@ impl Database {
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
connections.push(ConnectionId {
|
||||
connections.insert(ConnectionId {
|
||||
id: row.connection_id as u32,
|
||||
owner_id: row.connection_server_id.0 as u32,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((connections, channel_members, buffer.epoch, max_version))
|
||||
Ok((connections, buffer.epoch, max_version))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use rpc::{
|
||||
proto::{channel_member::Kind, ChannelBufferVersion, VectorClockEntry},
|
||||
ErrorCode, ErrorCodeExt,
|
||||
};
|
||||
use sea_orm::TryGetableMany;
|
||||
use sea_orm::{DbBackend, TryGetableMany};
|
||||
|
||||
impl Database {
|
||||
#[cfg(test)]
|
||||
@@ -700,77 +700,73 @@ impl Database {
|
||||
pub async fn get_channel_participant_details(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
filter: &str,
|
||||
limit: u64,
|
||||
user_id: UserId,
|
||||
) -> Result<Vec<proto::ChannelMember>> {
|
||||
let (role, members) = self
|
||||
) -> Result<(Vec<proto::ChannelMember>, Vec<proto::User>)> {
|
||||
let members = self
|
||||
.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
self.check_user_is_channel_participant(&channel, user_id, &tx)
|
||||
.await?;
|
||||
Ok((
|
||||
role,
|
||||
self.get_channel_participant_details_internal(&channel, &tx)
|
||||
.await?,
|
||||
))
|
||||
let mut query = channel_member::Entity::find()
|
||||
.find_also_related(user::Entity)
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()));
|
||||
|
||||
if cfg!(any(test, sqlite)) && self.pool.get_database_backend() == DbBackend::Sqlite {
|
||||
query = query.filter(Expr::cust_with_values(
|
||||
"UPPER(github_login) LIKE ?",
|
||||
[Self::fuzzy_like_string(&filter.to_uppercase())],
|
||||
))
|
||||
} else {
|
||||
query = query.filter(Expr::cust_with_values(
|
||||
"github_login ILIKE $1",
|
||||
[Self::fuzzy_like_string(filter)],
|
||||
))
|
||||
}
|
||||
let members = query.order_by(
|
||||
Expr::cust(
|
||||
"not role = 'admin', not role = 'member', not role = 'guest', not accepted, github_login",
|
||||
),
|
||||
sea_orm::Order::Asc,
|
||||
)
|
||||
.limit(limit)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(members)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if role == ChannelRole::Admin {
|
||||
Ok(members
|
||||
.into_iter()
|
||||
.map(|channel_member| proto::ChannelMember {
|
||||
role: channel_member.role.into(),
|
||||
user_id: channel_member.user_id.to_proto(),
|
||||
kind: if channel_member.accepted {
|
||||
let mut users: Vec<proto::User> = Vec::with_capacity(members.len());
|
||||
|
||||
let members = members
|
||||
.into_iter()
|
||||
.map(|(member, user)| {
|
||||
if let Some(user) = user {
|
||||
users.push(proto::User {
|
||||
id: user.id.to_proto(),
|
||||
avatar_url: format!(
|
||||
"https://github.com/{}.png?size=128",
|
||||
user.github_login
|
||||
),
|
||||
github_login: user.github_login,
|
||||
})
|
||||
}
|
||||
proto::ChannelMember {
|
||||
role: member.role.into(),
|
||||
user_id: member.user_id.to_proto(),
|
||||
kind: if member.accepted {
|
||||
Kind::Member
|
||||
} else {
|
||||
Kind::Invitee
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
return Ok(members
|
||||
.into_iter()
|
||||
.filter_map(|member| {
|
||||
if !member.accepted {
|
||||
return None;
|
||||
}
|
||||
Some(proto::ChannelMember {
|
||||
role: member.role.into(),
|
||||
user_id: member.user_id.to_proto(),
|
||||
kind: Kind::Member.into(),
|
||||
})
|
||||
})
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
async fn get_channel_participant_details_internal(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<channel_member::Model>> {
|
||||
Ok(channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Returns the participants in the given channel.
|
||||
pub async fn get_channel_participants(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<UserId>> {
|
||||
let participants = self
|
||||
.get_channel_participant_details_internal(channel, tx)
|
||||
.await?;
|
||||
Ok(participants
|
||||
.into_iter()
|
||||
.map(|member| member.user_id)
|
||||
.collect())
|
||||
Ok((members, users))
|
||||
}
|
||||
|
||||
/// Returns whether the given user is an admin in the specified channel.
|
||||
|
||||
@@ -73,6 +73,7 @@ impl Database {
|
||||
pub async fn create_dev_server(
|
||||
&self,
|
||||
name: &str,
|
||||
ssh_connection_string: Option<&str>,
|
||||
hashed_access_token: &str,
|
||||
user_id: UserId,
|
||||
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
|
||||
@@ -86,6 +87,9 @@ impl Database {
|
||||
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
|
||||
name: ActiveValue::Set(name.trim().to_string()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
ssh_connection_string: ActiveValue::Set(
|
||||
ssh_connection_string.map(ToOwned::to_owned),
|
||||
),
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -251,7 +251,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut participant_connection_ids = Vec::new();
|
||||
let mut participant_connection_ids = HashSet::default();
|
||||
let mut participant_user_ids = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
@@ -259,7 +259,7 @@ impl Database {
|
||||
is_participant = true;
|
||||
}
|
||||
participant_user_ids.push(row.user_id);
|
||||
participant_connection_ids.push(row.connection());
|
||||
participant_connection_ids.insert(row.connection());
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
@@ -336,13 +336,9 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
let mut channel_members = self.get_channel_participants(&channel, &tx).await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok(CreatedChannelMessage {
|
||||
message_id,
|
||||
participant_connection_ids,
|
||||
channel_members,
|
||||
notifications,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,6 +48,9 @@ impl Database {
|
||||
|
||||
/// Returns all users by ID. There are no access checks here, so this should only be used internally.
|
||||
pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
|
||||
if ids.len() >= 10000_usize {
|
||||
return Err(anyhow!("too many users"))?;
|
||||
}
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
Ok(user::Entity::find()
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Model {
|
||||
pub name: String,
|
||||
pub user_id: UserId,
|
||||
pub hashed_token: String,
|
||||
pub ssh_connection_string: Option<String>,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -32,6 +33,7 @@ impl Model {
|
||||
dev_server_id: self.id.to_proto(),
|
||||
name: self.name.clone(),
|
||||
status: status as i32,
|
||||
ssh_connection_string: self.ssh_connection_string.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{
|
||||
tests::{channel_tree, new_test_connection, new_test_user},
|
||||
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
|
||||
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
@@ -40,15 +40,15 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.transaction(|tx| async move {
|
||||
let channel = db.get_channel_internal(replace_id, &tx).await?;
|
||||
db.get_channel_participants(&channel, &tx).await
|
||||
})
|
||||
let (members, _) = db
|
||||
.get_channel_participant_details(replace_id, "", 10, a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
members.sort();
|
||||
assert_eq!(members, &[a_id, b_id]);
|
||||
let ids = members
|
||||
.into_iter()
|
||||
.map(|m| UserId::from_proto(m.user_id))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ids, &[a_id, b_id]);
|
||||
|
||||
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
|
||||
let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap();
|
||||
@@ -195,8 +195,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(channel_1_1, user_1)
|
||||
let (mut members, _) = db
|
||||
.get_channel_participant_details(channel_1_1, "", 100, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -231,8 +231,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = db
|
||||
.get_channel_participant_details(channel_1_3, user_1)
|
||||
let (members, _) = db
|
||||
.get_channel_participant_details(channel_1_3, "", 100, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -243,16 +243,16 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -482,8 +482,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
let (mut members, _) = db
|
||||
.get_channel_participant_details(public_channel_id, "", 100, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -557,8 +557,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
let (mut members, _) = db
|
||||
.get_channel_participant_details(public_channel_id, "", 100, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -594,8 +594,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
let (mut members, _) = db
|
||||
.get_channel_participant_details(public_channel_id, "", 100, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -663,8 +663,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
let (mut members, _) = db
|
||||
.get_channel_participant_details(public_channel_id, "", 100, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -2365,7 +2365,12 @@ async fn create_dev_server(
|
||||
let (dev_server, status) = session
|
||||
.db()
|
||||
.await
|
||||
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
|
||||
.create_dev_server(
|
||||
&request.name,
|
||||
request.ssh_connection_string.as_deref(),
|
||||
&hashed_access_token,
|
||||
session.user_id(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_dev_server_projects_update(session.user_id(), status, &session).await;
|
||||
@@ -2373,7 +2378,7 @@ async fn create_dev_server(
|
||||
response.send(proto::CreateDevServerResponse {
|
||||
dev_server_id: dev_server.id.0 as u64,
|
||||
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
|
||||
name: request.name.clone(),
|
||||
name: request.name,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -3683,10 +3688,15 @@ async fn get_channel_members(
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let members = db
|
||||
.get_channel_participant_details(channel_id, session.user_id())
|
||||
let limit = if request.limit == 0 {
|
||||
u16::MAX as u64
|
||||
} else {
|
||||
request.limit
|
||||
};
|
||||
let (members, users) = db
|
||||
.get_channel_participant_details(channel_id, &request.query, limit, session.user_id())
|
||||
.await?;
|
||||
response.send(proto::GetChannelMembersResponse { members })?;
|
||||
response.send(proto::GetChannelMembersResponse { members, users })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3886,13 +3896,13 @@ async fn update_channel_buffer(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
|
||||
let (collaborators, non_collaborators, epoch, version) = db
|
||||
let (collaborators, epoch, version) = db
|
||||
.update_channel_buffer(channel_id, session.user_id(), &request.operations)
|
||||
.await?;
|
||||
|
||||
channel_buffer_updated(
|
||||
session.connection_id,
|
||||
collaborators,
|
||||
collaborators.clone(),
|
||||
&proto::UpdateChannelBuffer {
|
||||
channel_id: channel_id.to_proto(),
|
||||
operations: request.operations,
|
||||
@@ -3902,25 +3912,29 @@ async fn update_channel_buffer(
|
||||
|
||||
let pool = &*session.connection_pool().await;
|
||||
|
||||
broadcast(
|
||||
None,
|
||||
non_collaborators
|
||||
.iter()
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: epoch as u64,
|
||||
version: version.clone(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
let non_collaborators =
|
||||
pool.channel_connection_ids(channel_id)
|
||||
.filter_map(|(connection_id, _)| {
|
||||
if collaborators.contains(&connection_id) {
|
||||
None
|
||||
} else {
|
||||
Some(connection_id)
|
||||
}
|
||||
});
|
||||
|
||||
broadcast(None, non_collaborators, |peer_id| {
|
||||
session.peer.send(
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: epoch as u64,
|
||||
version: version.clone(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4048,7 +4062,6 @@ async fn send_channel_message(
|
||||
let CreatedChannelMessage {
|
||||
message_id,
|
||||
participant_connection_ids,
|
||||
channel_members,
|
||||
notifications,
|
||||
} = session
|
||||
.db()
|
||||
@@ -4079,7 +4092,7 @@ async fn send_channel_message(
|
||||
};
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
participant_connection_ids,
|
||||
participant_connection_ids.clone(),
|
||||
|connection| {
|
||||
session.peer.send(
|
||||
connection,
|
||||
@@ -4095,24 +4108,27 @@ async fn send_channel_message(
|
||||
})?;
|
||||
|
||||
let pool = &*session.connection_pool().await;
|
||||
broadcast(
|
||||
None,
|
||||
channel_members
|
||||
.iter()
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_message_ids: vec![proto::ChannelMessageId {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message_id: message_id.to_proto(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
let non_participants =
|
||||
pool.channel_connection_ids(channel_id)
|
||||
.filter_map(|(connection_id, _)| {
|
||||
if participant_connection_ids.contains(&connection_id) {
|
||||
None
|
||||
} else {
|
||||
Some(connection_id)
|
||||
}
|
||||
});
|
||||
broadcast(None, non_participants, |peer_id| {
|
||||
session.peer.send(
|
||||
peer_id,
|
||||
proto::UpdateChannels {
|
||||
latest_channel_message_ids: vec![proto::ChannelMessageId {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message_id: message_id.to_proto(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
});
|
||||
send_notifications(pool, &session.peer, notifications);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -99,7 +99,7 @@ async fn test_core_channels(
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
store.get_channel_member_details(channel_a_id, cx)
|
||||
store.fuzzy_search_members(channel_a_id, "".to_string(), 10, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -20,7 +20,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
|
||||
|
||||
let resp = store
|
||||
.update(cx, |store, cx| {
|
||||
store.create_dev_server("server-1".to_string(), cx)
|
||||
store.create_dev_server("server-1".to_string(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -167,7 +167,7 @@ async fn create_dev_server_project(
|
||||
|
||||
let resp = store
|
||||
.update(cx, |store, cx| {
|
||||
store.create_dev_server("server-1".to_string(), cx)
|
||||
store.create_dev_server("server-1".to_string(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -521,7 +521,7 @@ async fn test_create_dev_server_project_path_validation(
|
||||
|
||||
let resp = store
|
||||
.update(cx1, |store, cx| {
|
||||
store.create_dev_server("server-2".to_string(), cx)
|
||||
store.create_dev_server("server-2".to_string(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -19,7 +19,7 @@ use editor::{
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext};
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, InlayHintSettings},
|
||||
@@ -1517,7 +1517,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
cx_b.update(editor::init);
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
@@ -1531,7 +1531,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
@@ -1779,7 +1779,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
cx_b.update(editor::init);
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: false,
|
||||
@@ -1793,7 +1793,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
@@ -2269,14 +2269,14 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
min_column: None,
|
||||
});
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||
settings.git.inline_blame = inline_blame_off_settings;
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||
settings.git.inline_blame = inline_blame_off_settings;
|
||||
});
|
||||
|
||||
@@ -13,11 +13,11 @@ use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::{channel::mpsc, StreamExt as _};
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
|
||||
MouseDownEvent, TestAppContext,
|
||||
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
|
||||
TestAppContext, UpdateGlobal,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, Formatter},
|
||||
language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
|
||||
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
|
||||
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
@@ -4401,7 +4401,7 @@ async fn test_formatting_buffer(
|
||||
// Ensure buffer can be formatted using an external command. Notice how the
|
||||
// host's configuration is honored as opposed to using the guest's settings.
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::External {
|
||||
command: "awk".into(),
|
||||
@@ -4445,18 +4445,17 @@ async fn test_prettier_formatting_buffer(
|
||||
|
||||
client_a.language_registry().add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
@@ -4470,11 +4469,11 @@ async fn test_prettier_formatting_buffer(
|
||||
let buffer_text = "let one = \"two\"";
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(&directory, json!({ "a.rs": buffer_text }))
|
||||
.insert_tree(&directory, json!({ "a.ts": buffer_text }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
|
||||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
@@ -4482,20 +4481,28 @@ async fn test_prettier_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::Auto);
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::LanguageServer);
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ auto_update.workspace = true
|
||||
call.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
|
||||
@@ -78,12 +78,14 @@ impl ChatPanel {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
let input_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
languages.clone(),
|
||||
channel_store.clone(),
|
||||
user_store.clone(),
|
||||
None,
|
||||
cx.new_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx,
|
||||
)
|
||||
@@ -231,19 +233,12 @@ impl ChatPanel {
|
||||
|
||||
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
|
||||
let channel_id = chat.read(cx).channel_id;
|
||||
{
|
||||
self.markdown_data.clear();
|
||||
|
||||
let chat = chat.read(cx);
|
||||
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
|
||||
let message_count = chat.message_count();
|
||||
self.message_list.reset(message_count);
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_channel(channel_id, channel_name, cx);
|
||||
editor.clear_reply_to_message_id();
|
||||
});
|
||||
};
|
||||
self.markdown_data.clear();
|
||||
self.message_list.reset(chat.read(cx).message_count());
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_channel_chat(chat.clone(), cx);
|
||||
editor.clear_reply_to_message_id();
|
||||
});
|
||||
let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
||||
self.active_chat = Some((chat, subscription));
|
||||
self.acknowledge_last_message(cx);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use channel::{ChannelMembership, ChannelStore, MessageParams};
|
||||
use client::{ChannelId, UserId};
|
||||
use collections::{HashMap, HashSet};
|
||||
use channel::{ChannelChat, ChannelStore, MessageParams};
|
||||
use client::{UserId, UserStore};
|
||||
use collections::HashSet;
|
||||
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
Render, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
@@ -31,11 +31,10 @@ lazy_static! {
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: View<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_members: HashMap<String, UserId>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_chat: Option<Model<ChannelChat>>,
|
||||
mentions: Vec<UserId>,
|
||||
mentions_task: Option<Task<()>>,
|
||||
channel_id: Option<ChannelId>,
|
||||
reply_to_message_id: Option<u64>,
|
||||
edit_message_id: Option<u64>,
|
||||
}
|
||||
@@ -81,7 +80,8 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_chat: Option<Model<ChannelChat>>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
@@ -127,9 +127,8 @@ impl MessageEditor {
|
||||
|
||||
Self {
|
||||
editor,
|
||||
channel_store,
|
||||
channel_members: HashMap::default(),
|
||||
channel_id: None,
|
||||
user_store,
|
||||
channel_chat,
|
||||
mentions: Vec::new(),
|
||||
mentions_task: None,
|
||||
reply_to_message_id: None,
|
||||
@@ -161,12 +160,13 @@ impl MessageEditor {
|
||||
self.edit_message_id = None;
|
||||
}
|
||||
|
||||
pub fn set_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
channel_name: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
|
||||
let channel_id = chat.read(cx).channel_id;
|
||||
self.channel_chat = Some(chat);
|
||||
let channel_name = ChannelStore::global(cx)
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.name.clone());
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if let Some(channel_name) = channel_name {
|
||||
editor.set_placeholder_text(format!("Message #{channel_name}"), cx);
|
||||
@@ -174,31 +174,6 @@ impl MessageEditor {
|
||||
editor.set_placeholder_text("Message Channel", cx);
|
||||
}
|
||||
});
|
||||
self.channel_id = Some(channel_id);
|
||||
self.refresh_users(cx);
|
||||
}
|
||||
|
||||
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(channel_id) = self.channel_id {
|
||||
let members = self.channel_store.update(cx, |store, cx| {
|
||||
store.get_channel_member_details(channel_id, cx)
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let members = members.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
|
||||
self.channel_members.clear();
|
||||
self.channel_members.extend(
|
||||
members
|
||||
.into_iter()
|
||||
.map(|member| (member.user.github_login.clone(), member.user.id)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
|
||||
@@ -368,13 +343,19 @@ impl MessageEditor {
|
||||
let start_anchor = buffer.read(cx).anchor_before(start_offset);
|
||||
|
||||
let mut names = HashSet::default();
|
||||
for (github_login, _) in self.channel_members.iter() {
|
||||
names.insert(github_login.clone());
|
||||
}
|
||||
if let Some(channel_id) = self.channel_id {
|
||||
for participant in self.channel_store.read(cx).channel_participants(channel_id) {
|
||||
if let Some(chat) = self.channel_chat.as_ref() {
|
||||
let chat = chat.read(cx);
|
||||
for participant in ChannelStore::global(cx)
|
||||
.read(cx)
|
||||
.channel_participants(chat.channel_id)
|
||||
{
|
||||
names.insert(participant.github_login.clone());
|
||||
}
|
||||
for message in chat
|
||||
.messages_in_range(chat.message_count().saturating_sub(100)..chat.message_count())
|
||||
{
|
||||
names.insert(message.sender.github_login.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let candidates = names
|
||||
@@ -481,11 +462,15 @@ impl MessageEditor {
|
||||
text.clear();
|
||||
text.extend(buffer.text_for_range(range.clone()));
|
||||
if let Some(username) = text.strip_prefix('@') {
|
||||
if let Some(user_id) = this.channel_members.get(username) {
|
||||
if let Some(user) = this
|
||||
.user_store
|
||||
.read(cx)
|
||||
.cached_user_by_github_login(username)
|
||||
{
|
||||
let start = multi_buffer.anchor_after(range.start);
|
||||
let end = multi_buffer.anchor_after(range.end);
|
||||
|
||||
mentioned_user_ids.push(*user_id);
|
||||
mentioned_user_ids.push(user.id);
|
||||
anchor_ranges.push(start..end);
|
||||
}
|
||||
}
|
||||
@@ -550,106 +535,3 @@ impl Render for MessageEditor {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use http::FakeHttpClient;
|
||||
use language::{Language, LanguageConfig};
|
||||
use project::Project;
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||
let language_registry = init_test(cx);
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.new_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_members(
|
||||
vec![
|
||||
ChannelMembership {
|
||||
user: Arc::new(User {
|
||||
github_login: "a-b".into(),
|
||||
id: 101,
|
||||
avatar_uri: "avatar_a-b".into(),
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
},
|
||||
ChannelMembership {
|
||||
user: Arc::new(User {
|
||||
github_login: "C_D".into(),
|
||||
id: 102,
|
||||
avatar_uri: "avatar_C_D".into(),
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.editor.update(cx, |editor, cx| {
|
||||
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||
assert_eq!(
|
||||
editor.take_message(cx),
|
||||
MessageParams {
|
||||
text,
|
||||
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
|
||||
reply_to_message_id: None
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
channel::init(&client, user_store, cx);
|
||||
|
||||
MessageEditorSettings::register(cx);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Markdown".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_markdown::language()),
|
||||
)));
|
||||
language_registry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1569,11 +1569,28 @@ impl CollabPanel {
|
||||
|
||||
*pending_name = Some(channel_name.clone());
|
||||
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.create_channel(&channel_name, *location, cx)
|
||||
let create = self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store.create_channel(&channel_name, *location, cx)
|
||||
});
|
||||
if location.is_none() {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let channel_id = create.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.show_channel_modal(
|
||||
channel_id,
|
||||
channel_modal::Mode::InviteMembers,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
.detach_and_prompt_err(
|
||||
"Failed to create channel",
|
||||
cx,
|
||||
|_, _| None,
|
||||
);
|
||||
} else {
|
||||
create.detach_and_prompt_err("Failed to create channel", cx, |_, _| None);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
ChannelEditingState::Rename {
|
||||
@@ -1859,12 +1876,8 @@ impl CollabPanel {
|
||||
let workspace = self.workspace.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_store = self.channel_store.clone();
|
||||
let members = self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let members = members.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
ChannelModal::new(
|
||||
@@ -1872,7 +1885,6 @@ impl CollabPanel {
|
||||
channel_store.clone(),
|
||||
channel_id,
|
||||
mode,
|
||||
members,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -2949,7 +2961,7 @@ struct DraggedChannelView {
|
||||
}
|
||||
|
||||
impl Render for DraggedChannelView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
||||
h_flex()
|
||||
.font_family(ui_font)
|
||||
|
||||
@@ -37,7 +37,6 @@ impl ChannelModal {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
@@ -54,7 +53,8 @@ impl ChannelModal {
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
context_menu: None,
|
||||
members,
|
||||
members: Vec::new(),
|
||||
has_all_members: false,
|
||||
mode,
|
||||
},
|
||||
cx,
|
||||
@@ -78,37 +78,15 @@ impl ChannelModal {
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate.members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
|
||||
@@ -260,6 +238,7 @@ pub struct ChannelModalDelegate {
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
has_all_members: bool,
|
||||
context_menu: Option<(View<ContextMenu>, Subscription)>,
|
||||
}
|
||||
|
||||
@@ -288,37 +267,59 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
if self.has_all_members {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
} else {
|
||||
let search_members = self.channel_store.update(cx, |store, cx| {
|
||||
store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let members = search_members.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate.has_all_members =
|
||||
query == "" && members.len() < 100;
|
||||
picker.delegate.matching_member_indices =
|
||||
(0..members.len()).collect();
|
||||
picker.delegate.members = members;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
}
|
||||
Mode::InviteMembers => {
|
||||
let search_users = self
|
||||
|
||||
@@ -731,7 +731,7 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Themes...", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.separator()
|
||||
.action("Sign Out", client::SignOut.boxed_clone())
|
||||
})
|
||||
@@ -743,7 +743,11 @@ impl CollabTitlebarItem {
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
@@ -755,16 +759,18 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Themes...", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.trigger(
|
||||
ButtonLike::new("user-menu")
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
|
||||
h_flex().gap_0p5().child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
|
||||
|
||||
@@ -12,7 +12,7 @@ use command_palette_hooks::{
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
@@ -362,7 +362,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
HitCounts::update_global(cx, |hit_counts, _cx| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
let action = command.action;
|
||||
|
||||
@@ -59,6 +59,10 @@ impl CopilotCompletionProvider {
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
fn name() -> &'static str {
|
||||
"copilot"
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -186,17 +190,23 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_copilot_event(
|
||||
Some(completion.uuid.clone()),
|
||||
true,
|
||||
self.file_extension.clone(),
|
||||
);
|
||||
if self.active_completion().is_some() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_inline_completion_event(
|
||||
Self::name().to_string(),
|
||||
true,
|
||||
self.file_extension.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn discard(
|
||||
&mut self,
|
||||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.inline_completions_enabled(None, None);
|
||||
@@ -210,8 +220,17 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_copilot_event(None, false, self.file_extension.clone());
|
||||
|
||||
if should_report_inline_completion_event {
|
||||
if self.active_completion().is_some() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_inline_completion_event(
|
||||
Self::name().to_string(),
|
||||
false,
|
||||
self.file_extension.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +292,7 @@ mod tests {
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{BackgroundExecutor, BorrowAppContext, Context, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
@@ -1138,7 +1157,7 @@ mod tests {
|
||||
editor::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ impl From<proto::DevServerProject> for DevServerProject {
|
||||
pub struct DevServer {
|
||||
pub id: DevServerId,
|
||||
pub name: SharedString,
|
||||
pub ssh_connection_string: Option<SharedString>,
|
||||
pub status: DevServerStatus,
|
||||
}
|
||||
|
||||
@@ -48,6 +49,7 @@ impl From<proto::DevServer> for DevServer {
|
||||
id: DevServerId(dev_server.dev_server_id),
|
||||
status: dev_server.status(),
|
||||
name: dev_server.name.into(),
|
||||
ssh_connection_string: dev_server.ssh_connection_string.map(|s| s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,11 +166,17 @@ impl Store {
|
||||
pub fn create_dev_server(
|
||||
&mut self,
|
||||
name: String,
|
||||
ssh_connection_string: Option<String>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<proto::CreateDevServerResponse>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let result = client.request(proto::CreateDevServer { name }).await?;
|
||||
let result = client
|
||||
.request(proto::CreateDevServer {
|
||||
name,
|
||||
ssh_connection_string,
|
||||
})
|
||||
.await?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ struct DiagnosticGroupState {
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let child = if self.path_states.is_empty() {
|
||||
div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
|
||||
@@ -48,7 +48,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
use debounced_delay::DebouncedDelay;
|
||||
pub use display_map::DisplayPoint;
|
||||
@@ -302,6 +302,7 @@ pub enum SelectPhase {
|
||||
},
|
||||
BeginColumnar {
|
||||
position: DisplayPoint,
|
||||
reset: bool,
|
||||
goal_column: u32,
|
||||
},
|
||||
Extend {
|
||||
@@ -450,7 +451,7 @@ pub struct Editor {
|
||||
show_wrap_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlight_order: usize,
|
||||
highlighted_rows: HashMap<TypeId, Vec<(usize, RangeInclusive<Anchor>, Option<Hsla>)>>,
|
||||
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
|
||||
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
|
||||
scrollbar_marker_state: ScrollbarMarkerState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
@@ -660,6 +661,13 @@ impl SelectionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
struct RowHighlight {
|
||||
index: usize,
|
||||
range: RangeInclusive<Anchor>,
|
||||
color: Option<Hsla>,
|
||||
should_autoscroll: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AddSelectionsState {
|
||||
above: bool,
|
||||
@@ -1579,7 +1587,25 @@ impl Editor {
|
||||
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
|
||||
if let project::Event::RefreshInlayHints = event {
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
|
||||
};
|
||||
} else if let project::Event::SnippetEdit(id, snippet_edits) = event {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
if focus_handle.is_focused(cx) {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
for (range, snippet) in snippet_edits {
|
||||
let editor_range =
|
||||
language::range_from_lsp(*range).to_offset(&snapshot);
|
||||
editor
|
||||
.insert_snippet(&[editor_range], snippet.clone(), cx)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
let task_inventory = project.read(cx).task_inventory().clone();
|
||||
project_subscriptions.push(cx.observe(&task_inventory, |editor, _, cx| {
|
||||
editor.tasks_update_task = Some(editor.refresh_runnables(cx));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1589,7 +1615,6 @@ impl Editor {
|
||||
&buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
);
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, Self::handle_focus).detach();
|
||||
cx.on_blur(&focus_handle, Self::handle_blur).detach();
|
||||
@@ -1717,6 +1742,12 @@ impl Editor {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn mouse_menu_is_focused(&self, cx: &mut WindowContext) -> bool {
|
||||
self.mouse_context_menu
|
||||
.as_ref()
|
||||
.is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(cx))
|
||||
}
|
||||
|
||||
fn key_context(&self, cx: &AppContext) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("Editor");
|
||||
@@ -2024,6 +2055,7 @@ impl Editor {
|
||||
&mut self,
|
||||
local: bool,
|
||||
old_cursor_position: &Anchor,
|
||||
show_completions: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// Copy selections to primary selection buffer
|
||||
@@ -2125,7 +2157,9 @@ impl Editor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.show_completions(&ShowCompletions, cx);
|
||||
if show_completions {
|
||||
self.show_completions(&ShowCompletions, cx);
|
||||
}
|
||||
} else {
|
||||
drop(context_menu);
|
||||
self.hide_context_menu(cx);
|
||||
@@ -2144,7 +2178,7 @@ impl Editor {
|
||||
self.refresh_code_actions(cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.discard_inline_completion(cx);
|
||||
self.discard_inline_completion(false, cx);
|
||||
if self.git_blame_inline_enabled {
|
||||
self.start_inline_blame_timer(cx);
|
||||
}
|
||||
@@ -2165,6 +2199,16 @@ impl Editor {
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
|
||||
) -> R {
|
||||
self.change_selections_inner(autoscroll, true, cx, change)
|
||||
}
|
||||
|
||||
pub fn change_selections_inner<R>(
|
||||
&mut self,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
request_completions: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
|
||||
) -> R {
|
||||
let old_cursor_position = self.selections.newest_anchor().head();
|
||||
self.push_to_selection_history();
|
||||
@@ -2175,7 +2219,7 @@ impl Editor {
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
self.selections_did_change(true, &old_cursor_position, cx);
|
||||
self.selections_did_change(true, &old_cursor_position, request_completions, cx);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -2247,7 +2291,8 @@ impl Editor {
|
||||
SelectPhase::BeginColumnar {
|
||||
position,
|
||||
goal_column,
|
||||
} => self.begin_columnar_selection(position, goal_column, cx),
|
||||
reset,
|
||||
} => self.begin_columnar_selection(position, goal_column, reset, cx),
|
||||
SelectPhase::Extend {
|
||||
position,
|
||||
click_count,
|
||||
@@ -2367,6 +2412,7 @@ impl Editor {
|
||||
&mut self,
|
||||
position: DisplayPoint,
|
||||
goal_column: u32,
|
||||
reset: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.focus_handle.is_focused(cx) {
|
||||
@@ -2374,16 +2420,33 @@ impl Editor {
|
||||
}
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
if reset {
|
||||
let pointer_position = display_map
|
||||
.buffer_snapshot
|
||||
.anchor_before(position.to_point(&display_map));
|
||||
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.clear_disjoint();
|
||||
s.set_pending_anchor_range(
|
||||
pointer_position..pointer_position,
|
||||
SelectMode::Character,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let tail = self.selections.newest::<Point>(cx).tail();
|
||||
self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));
|
||||
|
||||
self.select_columns(
|
||||
tail.to_display_point(&display_map),
|
||||
position,
|
||||
goal_column,
|
||||
&display_map,
|
||||
cx,
|
||||
);
|
||||
if !reset {
|
||||
self.select_columns(
|
||||
tail.to_display_point(&display_map),
|
||||
position,
|
||||
goal_column,
|
||||
&display_map,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
@@ -2549,7 +2612,7 @@ impl Editor {
|
||||
|
||||
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.clear_expanded_diff_hunks(cx);
|
||||
if self.dismiss_menus_and_popups(cx) {
|
||||
if self.dismiss_menus_and_popups(true, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2562,7 +2625,11 @@ impl Editor {
|
||||
cx.propagate();
|
||||
}
|
||||
|
||||
pub fn dismiss_menus_and_popups(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
pub fn dismiss_menus_and_popups(
|
||||
&mut self,
|
||||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if self.take_rename(false, cx).is_some() {
|
||||
return true;
|
||||
}
|
||||
@@ -2575,7 +2642,7 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.discard_inline_completion(cx) {
|
||||
if self.discard_inline_completion(should_report_inline_completion_event, cx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2831,7 +2898,9 @@ impl Editor {
|
||||
|
||||
drop(snapshot);
|
||||
let had_active_inline_completion = this.has_active_inline_completion(cx);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
||||
if brace_inserted {
|
||||
// If we inserted a brace while composing text (i.e. typing `"` on a
|
||||
@@ -3687,7 +3756,7 @@ impl Editor {
|
||||
let menu = menu.unwrap();
|
||||
*context_menu = Some(ContextMenu::Completions(menu));
|
||||
drop(context_menu);
|
||||
this.discard_inline_completion(cx);
|
||||
this.discard_inline_completion(false, cx);
|
||||
cx.notify();
|
||||
} else if this.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
@@ -3901,7 +3970,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
this.completion_tasks.clear();
|
||||
this.discard_inline_completion(cx);
|
||||
this.discard_inline_completion(false, cx);
|
||||
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||
|(tasks, (workspace, _))| {
|
||||
let position = Point::new(buffer_row, tasks.1.column);
|
||||
@@ -4038,7 +4107,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_project_transaction(
|
||||
pub async fn open_project_transaction(
|
||||
this: &WeakView<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
transaction: ProjectTransaction,
|
||||
@@ -4296,7 +4365,7 @@ impl Editor {
|
||||
if !self.show_inline_completions
|
||||
|| !provider.is_enabled(&buffer, cursor_buffer_position, cx)
|
||||
{
|
||||
self.discard_inline_completion(cx);
|
||||
self.discard_inline_completion(false, cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -4431,9 +4500,13 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn discard_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
fn discard_inline_completion(
|
||||
&mut self,
|
||||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.discard(cx);
|
||||
provider.discard(should_report_inline_completion_event, cx);
|
||||
}
|
||||
|
||||
self.take_active_inline_completion(cx).is_some()
|
||||
@@ -4496,7 +4569,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.discard_inline_completion(cx);
|
||||
self.discard_inline_completion(false, cx);
|
||||
}
|
||||
|
||||
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
|
||||
@@ -4550,7 +4623,7 @@ impl Editor {
|
||||
row: DisplayRow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> IconButton {
|
||||
IconButton::new("run_indicator", ui::IconName::Play)
|
||||
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -8355,7 +8428,25 @@ impl Editor {
|
||||
|
||||
let range = target.range.to_offset(target.buffer.read(cx));
|
||||
let range = editor.range_for_match(&range);
|
||||
|
||||
/// If select range has more than one line, we
|
||||
/// just point the cursor to range.start.
|
||||
fn check_multiline_range(
|
||||
buffer: &Buffer,
|
||||
range: Range<usize>,
|
||||
) -> Range<usize> {
|
||||
if buffer.offset_to_point(range.start).row
|
||||
== buffer.offset_to_point(range.end).row
|
||||
{
|
||||
range
|
||||
} else {
|
||||
range.start..range.start
|
||||
}
|
||||
}
|
||||
|
||||
if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
|
||||
let buffer = target.buffer.read(cx);
|
||||
let range = check_multiline_range(buffer, range);
|
||||
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
@@ -8375,6 +8466,8 @@ impl Editor {
|
||||
// When selecting a definition in a different buffer, disable the nav history
|
||||
// to avoid creating a history entry at the previous cursor location.
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
let buffer = target.buffer.read(cx);
|
||||
let range = check_multiline_range(buffer, range);
|
||||
target_editor.change_selections(
|
||||
Some(Autoscroll::focused()),
|
||||
cx,
|
||||
@@ -9120,7 +9213,7 @@ impl Editor {
|
||||
s.clear_pending();
|
||||
}
|
||||
});
|
||||
self.selections_did_change(false, &old_cursor_position, cx);
|
||||
self.selections_did_change(false, &old_cursor_position, true, cx);
|
||||
}
|
||||
|
||||
fn push_to_selection_history(&mut self) {
|
||||
@@ -9784,47 +9877,30 @@ impl Editor {
|
||||
&mut self,
|
||||
rows: RangeInclusive<Anchor>,
|
||||
color: Option<Hsla>,
|
||||
should_autoscroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
match self.highlighted_rows.entry(TypeId::of::<T>()) {
|
||||
hash_map::Entry::Occupied(o) => {
|
||||
let row_highlights = o.into_mut();
|
||||
let existing_highlight_index =
|
||||
row_highlights.binary_search_by(|(_, highlight_range, _)| {
|
||||
highlight_range
|
||||
.start()
|
||||
.cmp(&rows.start(), &multi_buffer_snapshot)
|
||||
.then(
|
||||
highlight_range
|
||||
.end()
|
||||
.cmp(&rows.end(), &multi_buffer_snapshot),
|
||||
)
|
||||
});
|
||||
match color {
|
||||
Some(color) => {
|
||||
let insert_index = match existing_highlight_index {
|
||||
Ok(i) => i,
|
||||
Err(i) => i,
|
||||
};
|
||||
row_highlights.insert(
|
||||
insert_index,
|
||||
(post_inc(&mut self.highlight_order), rows, Some(color)),
|
||||
);
|
||||
}
|
||||
None => match existing_highlight_index {
|
||||
Ok(i) => {
|
||||
row_highlights.remove(i);
|
||||
}
|
||||
Err(i) => {
|
||||
row_highlights
|
||||
.insert(i, (post_inc(&mut self.highlight_order), rows, None));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(vec![(post_inc(&mut self.highlight_order), rows, color)]);
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let row_highlights = self.highlighted_rows.entry(TypeId::of::<T>()).or_default();
|
||||
let existing_highlight_index = row_highlights.binary_search_by(|highlight| {
|
||||
highlight
|
||||
.range
|
||||
.start()
|
||||
.cmp(&rows.start(), &snapshot)
|
||||
.then(highlight.range.end().cmp(&rows.end(), &snapshot))
|
||||
});
|
||||
match (color, existing_highlight_index) {
|
||||
(Some(_), Ok(ix)) | (_, Err(ix)) => row_highlights.insert(
|
||||
ix,
|
||||
RowHighlight {
|
||||
index: post_inc(&mut self.highlight_order),
|
||||
range: rows,
|
||||
should_autoscroll,
|
||||
color,
|
||||
},
|
||||
),
|
||||
(None, Ok(i)) => {
|
||||
row_highlights.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9842,7 +9918,7 @@ impl Editor {
|
||||
self.highlighted_rows
|
||||
.get(&TypeId::of::<T>())?
|
||||
.iter()
|
||||
.map(|(_, range, color)| (range, color.as_ref())),
|
||||
.map(|highlight| (&highlight.range, highlight.color.as_ref())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9851,33 +9927,27 @@ impl Editor {
|
||||
/// Allows to ignore certain kinds of highlights.
|
||||
pub fn highlighted_display_rows(
|
||||
&mut self,
|
||||
exclude_highlights: HashSet<TypeId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> BTreeMap<DisplayRow, Hsla> {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let mut used_highlight_orders = HashMap::default();
|
||||
self.highlighted_rows
|
||||
.iter()
|
||||
.filter(|(type_id, _)| !exclude_highlights.contains(type_id))
|
||||
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
|
||||
.fold(
|
||||
BTreeMap::<DisplayRow, Hsla>::new(),
|
||||
|mut unique_rows, (highlight_order, anchor_range, hsla)| {
|
||||
let start_row = anchor_range.start().to_display_point(&snapshot).row();
|
||||
let end_row = anchor_range.end().to_display_point(&snapshot).row();
|
||||
|mut unique_rows, highlight| {
|
||||
let start_row = highlight.range.start().to_display_point(&snapshot).row();
|
||||
let end_row = highlight.range.end().to_display_point(&snapshot).row();
|
||||
for row in start_row.0..=end_row.0 {
|
||||
let used_index =
|
||||
used_highlight_orders.entry(row).or_insert(*highlight_order);
|
||||
if highlight_order >= used_index {
|
||||
*used_index = *highlight_order;
|
||||
match hsla {
|
||||
Some(hsla) => {
|
||||
unique_rows.insert(DisplayRow(row), *hsla);
|
||||
}
|
||||
None => {
|
||||
unique_rows.remove(&DisplayRow(row));
|
||||
}
|
||||
}
|
||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
if highlight.index >= *used_index {
|
||||
*used_index = highlight.index;
|
||||
match highlight.color {
|
||||
Some(hsla) => unique_rows.insert(DisplayRow(row), hsla),
|
||||
None => unique_rows.remove(&DisplayRow(row)),
|
||||
};
|
||||
}
|
||||
}
|
||||
unique_rows
|
||||
@@ -9885,6 +9955,22 @@ impl Editor {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn highlighted_display_row_for_autoscroll(
|
||||
&self,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> Option<DisplayRow> {
|
||||
self.highlighted_rows
|
||||
.values()
|
||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||
.filter_map(|highlight| {
|
||||
if highlight.color.is_none() || !highlight.should_autoscroll {
|
||||
return None;
|
||||
}
|
||||
Some(highlight.range.start().to_display_point(&snapshot).row())
|
||||
})
|
||||
.min()
|
||||
}
|
||||
|
||||
pub fn set_search_within_ranges(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
@@ -9897,6 +9983,10 @@ impl Editor {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn clear_search_within_ranges(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.clear_background_highlights::<SearchWithinRange>(cx);
|
||||
}
|
||||
|
||||
pub fn highlight_background<T: 'static>(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
@@ -10655,7 +10745,6 @@ impl Editor {
|
||||
|
||||
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(EditorEvent::Focused);
|
||||
|
||||
if let Some(rename) = self.pending_rename.as_ref() {
|
||||
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
|
||||
cx.focus(&rename_editor_focus_handle);
|
||||
|
||||
@@ -9,10 +9,12 @@ use crate::{
|
||||
JoinLines,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
|
||||
use gpui::{div, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
|
||||
},
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
|
||||
@@ -6254,13 +6256,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
// Enable Prettier formatting for the same buffer, and ensure
|
||||
// LSP is called instead of Prettier.
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
update_test_language_settings(cx, |settings| {
|
||||
// Enable Prettier formatting for the same buffer, and ensure
|
||||
// LSP is called instead of Prettier.
|
||||
settings.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
});
|
||||
});
|
||||
let mut fake_servers = language_registry.register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
@@ -6524,6 +6531,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
@@ -6539,10 +6547,13 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
three
|
||||
"},
|
||||
vec!["first_completion", "second_completion"],
|
||||
counter.clone(),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
@@ -6613,10 +6624,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
additional edit
|
||||
"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
counter.clone(),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
|
||||
@@ -6629,10 +6642,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
additional edit
|
||||
"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
counter.clone(),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
|
||||
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
@@ -6667,9 +6682,17 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
});
|
||||
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
"editor.<clo|>",
|
||||
vec!["close", "clobber"],
|
||||
counter.clone(),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
|
||||
|
||||
let apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
@@ -6680,6 +6703,103 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
|
||||
cx.simulate_keystroke(".");
|
||||
let completion_item = lsp::CompletionItem {
|
||||
label: "Some".into(),
|
||||
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||
detail: Some("Wrap the expression in an `Option::Some`".to_string()),
|
||||
documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "```rust\nSome(2)\n```".to_string(),
|
||||
})),
|
||||
deprecated: Some(false),
|
||||
sort_text: Some("Some".to_string()),
|
||||
filter_text: Some("Some".to_string()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
},
|
||||
new_text: "Some(2)".to_string(),
|
||||
})),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 20,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
},
|
||||
new_text: "".to_string(),
|
||||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let closure_completion_item = completion_item.clone();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let counter_clone = counter.clone();
|
||||
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let task_completion_item = closure_completion_item.clone();
|
||||
counter_clone.fetch_add(1, atomic::Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
task_completion_item,
|
||||
])))
|
||||
}
|
||||
});
|
||||
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
|
||||
assert!(request.next().await.is_some());
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
cx.simulate_keystroke("S");
|
||||
cx.simulate_keystroke("o");
|
||||
cx.simulate_keystroke("m");
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
|
||||
assert!(request.next().await.is_some());
|
||||
assert!(request.next().await.is_some());
|
||||
assert!(request.next().await.is_some());
|
||||
request.close();
|
||||
assert!(request.next().await.is_none());
|
||||
assert_eq!(
|
||||
counter.load(atomic::Ordering::Acquire),
|
||||
4,
|
||||
"With the completions menu open, only one LSP request should happen per input"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -8599,27 +8719,32 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
fs.insert_file("/file.ts", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
prettier_parser_name: Some("test_parser".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)));
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
});
|
||||
});
|
||||
|
||||
let test_plugin = "test_plugin";
|
||||
let _ = language_registry.register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
@@ -8628,7 +8753,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -11346,6 +11471,7 @@ pub fn handle_completion_request(
|
||||
cx: &mut EditorLspTestContext,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
counter: Arc<AtomicUsize>,
|
||||
) -> impl Future<Output = ()> {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
@@ -11361,6 +11487,7 @@ pub fn handle_completion_request(
|
||||
|
||||
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||
let completions = completions.clone();
|
||||
counter.fetch_add(1, atomic::Ordering::Release);
|
||||
async move {
|
||||
assert_eq!(params.text_document_position.text_document.uri, url.clone());
|
||||
assert_eq!(
|
||||
@@ -11424,7 +11551,7 @@ pub(crate) fn update_test_language_settings(
|
||||
f: impl Fn(&mut AllLanguageSettingsContent),
|
||||
) {
|
||||
_ = cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||||
});
|
||||
});
|
||||
@@ -11435,7 +11562,7 @@ pub(crate) fn update_test_project_settings(
|
||||
f: impl Fn(&mut ProjectSettings),
|
||||
) {
|
||||
_ = cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, f);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -194,6 +194,7 @@ impl Editor {
|
||||
editor.highlight_rows::<DiffRowHighlight>(
|
||||
to_inclusive_row_range(removed_rows, &snapshot),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -269,6 +270,7 @@ impl Editor {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
|
||||
Some(added_hunk_color(cx)),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
None
|
||||
@@ -277,6 +279,7 @@ impl Editor {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
|
||||
Some(added_hunk_color(cx)),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
|
||||
@@ -476,6 +479,7 @@ impl Editor {
|
||||
editor.highlight_rows::<DiffRowHighlight>(
|
||||
to_inclusive_row_range(removed_rows, &snapshot),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -581,7 +585,7 @@ fn editor_with_deleted_text(
|
||||
.buffer_snapshot
|
||||
.anchor_after(editor.buffer.read(cx).len(cx));
|
||||
|
||||
editor.highlight_rows::<DiffRowHighlight>(start..=end, Some(deleted_color), cx);
|
||||
editor.highlight_rows::<DiffRowHighlight>(start..=end, Some(deleted_color), false, cx);
|
||||
|
||||
let subscription_editor = parent_editor.clone();
|
||||
editor._subscriptions.extend([
|
||||
|
||||
@@ -3,6 +3,7 @@ use gpui::{AppContext, Model, ModelContext};
|
||||
use language::Buffer;
|
||||
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
fn name() -> &'static str;
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -24,7 +25,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||
cx: &mut ModelContext<Self>,
|
||||
);
|
||||
fn accept(&mut self, cx: &mut ModelContext<Self>);
|
||||
fn discard(&mut self, cx: &mut ModelContext<Self>);
|
||||
fn discard(&mut self, should_report_inline_completion_event: bool, cx: &mut ModelContext<Self>);
|
||||
fn active_completion_text<'a>(
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -55,7 +56,7 @@ pub trait InlineCompletionProviderHandle {
|
||||
cx: &mut AppContext,
|
||||
);
|
||||
fn accept(&self, cx: &mut AppContext);
|
||||
fn discard(&self, cx: &mut AppContext);
|
||||
fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext);
|
||||
fn active_completion_text<'a>(
|
||||
&'a self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -105,8 +106,10 @@ where
|
||||
self.update(cx, |this, cx| this.accept(cx))
|
||||
}
|
||||
|
||||
fn discard(&self, cx: &mut AppContext) {
|
||||
self.update(cx, |this, cx| this.discard(cx))
|
||||
fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.discard(should_report_inline_completion_event, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn active_completion_text<'a>(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
Copy, Cut, DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition,
|
||||
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFinder, SelectMode,
|
||||
ToggleCodeActions,
|
||||
};
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
use workspace::OpenInTerminal;
|
||||
@@ -70,8 +71,10 @@ pub fn deploy_context_menu(
|
||||
s.set_pending_display_range(point..point, SelectMode::Character);
|
||||
});
|
||||
|
||||
let focus = cx.focused();
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
let builder = menu
|
||||
.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Go to Implementation", Box::new(GoToImplementation))
|
||||
@@ -83,8 +86,16 @@ pub fn deploy_context_menu(
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.action("Cut", Box::new(Cut))
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.action("Reveal in Finder", Box::new(RevealInFinder))
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal));
|
||||
match focus {
|
||||
Some(focus) => builder.context(focus),
|
||||
None => builder,
|
||||
}
|
||||
})
|
||||
};
|
||||
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use std::{any::TypeId, cmp, f32};
|
||||
|
||||
use collections::HashSet;
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt,
|
||||
};
|
||||
use gpui::{px, Bounds, Pixels, ViewContext};
|
||||
use language::Point;
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, DiffRowHighlight, DisplayRow, Editor, EditorMode,
|
||||
LineWithInvisibles, RowExt,
|
||||
};
|
||||
use std::{cmp, f32};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Autoscroll {
|
||||
@@ -107,14 +103,10 @@ impl Editor {
|
||||
|
||||
let mut target_top;
|
||||
let mut target_bottom;
|
||||
if let Some(first_highlighted_row) = &self
|
||||
.highlighted_display_rows(
|
||||
HashSet::from_iter(Some(TypeId::of::<DiffRowHighlight>())),
|
||||
cx,
|
||||
)
|
||||
.first_entry()
|
||||
if let Some(first_highlighted_row) =
|
||||
self.highlighted_display_row_for_autoscroll(&display_map)
|
||||
{
|
||||
target_top = first_highlighted_row.key().as_f32();
|
||||
target_top = first_highlighted_row.as_f32();
|
||||
target_bottom = target_top + 1.;
|
||||
} else {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
@@ -244,7 +236,10 @@ impl Editor {
|
||||
let mut target_left;
|
||||
let mut target_right;
|
||||
|
||||
if self.highlighted_rows.is_empty() {
|
||||
if self
|
||||
.highlighted_display_row_for_autoscroll(&display_map)
|
||||
.is_none()
|
||||
{
|
||||
target_left = px(f32::INFINITY);
|
||||
target_right = px(0.);
|
||||
for selection in selections {
|
||||
|
||||
@@ -168,8 +168,7 @@ pub fn expanded_hunks_background_highlights(
|
||||
|
||||
let mut range_start = 0;
|
||||
let mut previous_highlighted_row = None;
|
||||
for (highlighted_row, _) in editor.highlighted_display_rows(collections::HashSet::default(), cx)
|
||||
{
|
||||
for (highlighted_row, _) in editor.highlighted_display_rows(cx) {
|
||||
match previous_highlighted_row {
|
||||
Some(previous_row) => {
|
||||
if previous_row + 1 != highlighted_row.0 {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "event_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "./src/event_server.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,141 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, BorrowAppContext, Global, Task};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::{channel::Sender, io::AsyncWriteExt, stream::StreamExt};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub enum OutputEvent {
|
||||
Hello,
|
||||
Save { path: PathBuf },
|
||||
}
|
||||
|
||||
struct EventServer {
|
||||
handler: Option<IoHandler>,
|
||||
}
|
||||
|
||||
struct IoHandler {
|
||||
tx: Sender<OutputEvent>,
|
||||
_task: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl Global for EventServer {}
|
||||
|
||||
impl EventServer {
|
||||
pub fn send(&self, event: OutputEvent) {
|
||||
if let Some(handler) = &self.handler {
|
||||
if dbg!(handler.tx.receiver_count()) > 1 {
|
||||
handler.tx.try_send(event).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Default)]
|
||||
struct EventServerSettings {
|
||||
event_server: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Settings for EventServerSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(
|
||||
sources: settings::SettingsSources<Self::FileContent>,
|
||||
_cx: &mut AppContext,
|
||||
) -> gpui::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
EventServerSettings::register(cx);
|
||||
let mut socket_path = EventServerSettings::get_global(cx).event_server.clone();
|
||||
let mut server = EventServer::new();
|
||||
server.set_socket_path(socket_path.clone(), cx).log_err();
|
||||
cx.set_global(server);
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let new_socket_path = &EventServerSettings::get_global(cx).event_server;
|
||||
if *new_socket_path != socket_path {
|
||||
socket_path = new_socket_path.clone();
|
||||
cx.update_global(|server: &mut EventServer, cx| {
|
||||
server.set_socket_path(socket_path.clone(), cx).log_err();
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// TODO: Remove this test code
|
||||
cx.spawn(|cx| async move {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(2000))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.global::<EventServer>().send(OutputEvent::Hello);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
impl EventServer {
|
||||
pub fn new() -> Self {
|
||||
Self { handler: None }
|
||||
}
|
||||
|
||||
pub fn set_socket_path(&mut self, path: Option<PathBuf>, cx: &AppContext) -> Result<()> {
|
||||
if let Some(path) = path {
|
||||
let executor = cx.background_executor().clone();
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let _task = cx.background_executor().spawn(
|
||||
async move {
|
||||
smol::fs::remove_file(&path).await.ok();
|
||||
let listener = smol::net::unix::UnixListener::bind(&path)?;
|
||||
|
||||
let mut incoming = listener.incoming();
|
||||
while let Some(stream) = incoming.next().await {
|
||||
if let Some(mut stream) = stream.log_err() {
|
||||
let rx = rx.clone();
|
||||
executor
|
||||
.spawn(
|
||||
async move {
|
||||
while let Some(message) = rx.recv().await.ok() {
|
||||
stream
|
||||
.write_all(
|
||||
serde_json::to_string(&message)?.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
stream.write_all(b"\n").await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
|
||||
self.handler = Some(IoHandler { tx, _task });
|
||||
} else {
|
||||
self.handler = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ impl ExtensionBuilder {
|
||||
pub fn new(cache_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
cache_dir,
|
||||
http: http::client(),
|
||||
http: http::client(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ world extension {
|
||||
code: string,
|
||||
/// The spans to display in the label.
|
||||
spans: list<code-label-span>,
|
||||
/// The range of the code to include when filtering.
|
||||
/// The range of the displayed label to include when filtering.
|
||||
filter-range: range,
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("toml", &["Cargo.lock", "toml"]),
|
||||
("vue", &["vue"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("wit", &["wit"]),
|
||||
("zig", &["zig"]),
|
||||
];
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
|
||||
@@ -122,6 +122,7 @@ impl GoToLine {
|
||||
active_editor.highlight_rows::<GoToLineRowHighlights>(
|
||||
anchor..=anchor,
|
||||
Some(cx.theme().colors().editor_highlighted_line_background),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
@@ -219,17 +220,14 @@ impl Render for GoToLine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashSet;
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line_view_row_highlights(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -350,7 +348,7 @@ mod tests {
|
||||
fn highlighted_display_rows(editor: &View<Editor>, cx: &mut VisualTestContext) -> Vec<u32> {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.highlighted_display_rows(HashSet::default(), cx)
|
||||
.highlighted_display_rows(cx)
|
||||
.into_keys()
|
||||
.map(|r| r.0)
|
||||
.collect()
|
||||
|
||||
@@ -126,6 +126,7 @@ x11rb = { version = "0.13.0", features = [
|
||||
"resource_manager",
|
||||
] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = ["x11rb-xcb", "x11rb-client"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -29,8 +29,8 @@ use crate::{
|
||||
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
|
||||
PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
|
||||
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
|
||||
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
|
||||
};
|
||||
@@ -115,7 +115,7 @@ impl App {
|
||||
Self(AppContext::new(
|
||||
current_platform(),
|
||||
Arc::new(()),
|
||||
http::client(),
|
||||
http::client(None),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -502,6 +502,13 @@ impl AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns Ok() if the platform supports opening windows.
|
||||
/// This returns false (for example) on linux when we could
|
||||
/// not establish a connection to X or Wayland.
|
||||
pub fn can_open_windows(&self) -> anyhow::Result<()> {
|
||||
self.platform.can_open_windows()
|
||||
}
|
||||
|
||||
/// Instructs the platform to activate the application by bringing it to the foreground.
|
||||
pub fn activate(&self, ignoring_other_apps: bool) {
|
||||
self.platform.activate(ignoring_other_apps);
|
||||
@@ -547,6 +554,7 @@ impl AppContext {
|
||||
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
@@ -558,6 +566,7 @@ impl AppContext {
|
||||
|
||||
/// Reads data from the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_primary()
|
||||
}
|
||||
@@ -649,6 +658,11 @@ impl AppContext {
|
||||
self.platform.local_timezone()
|
||||
}
|
||||
|
||||
/// Updates the http client assigned to GPUI
|
||||
pub fn update_http_client(&mut self, new_client: Arc<dyn HttpClient>) {
|
||||
self.http_client = new_client;
|
||||
}
|
||||
|
||||
/// Returns the http client assigned to GPUI
|
||||
pub fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http_client.clone()
|
||||
@@ -1153,6 +1167,11 @@ impl AppContext {
|
||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
/// Sets the right click menu for the app icon in the dock
|
||||
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
|
||||
self.platform.set_dock_menu(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
/// Adds given path to the bottom of the list of recent paths for the application.
|
||||
/// The list is usually shown on the application icon's context menu in the dock,
|
||||
/// and allows to open the recent files via that context menu.
|
||||
|
||||
@@ -432,6 +432,19 @@ impl TextLayout {
|
||||
pub fn line_height(&self) -> Pixels {
|
||||
self.0.lock().as_ref().unwrap().line_height
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn text(&self) -> String {
|
||||
self.0
|
||||
.lock()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.lines
|
||||
.iter()
|
||||
.map(|s| s.text.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// A text element that can be interacted with.
|
||||
|
||||
62
crates/gpui/src/global.rs
Normal file
62
crates/gpui/src/global.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::{AppContext, BorrowAppContext};
|
||||
|
||||
/// A marker trait for types that can be stored in GPUI's global state.
|
||||
///
|
||||
/// This trait exists to provide type-safe access to globals by ensuring only
|
||||
/// types that implement [`Global`] can be used with the accessor methods. For
|
||||
/// example, trying to access a global with a type that does not implement
|
||||
/// [`Global`] will result in a compile-time error.
|
||||
///
|
||||
/// Implement this on types you want to store in the context as a global.
|
||||
///
|
||||
/// ## Restricting Access to Globals
|
||||
///
|
||||
/// In some situations you may need to store some global state, but want to
|
||||
/// restrict access to reading it or writing to it.
|
||||
///
|
||||
/// In these cases, Rust's visibility system can be used to restrict access to
|
||||
/// a global value. For example, you can create a private struct that implements
|
||||
/// [`Global`] and holds the global state. Then create a newtype struct that wraps
|
||||
/// the global type and create custom accessor methods to expose the desired subset
|
||||
/// of operations.
|
||||
pub trait Global: 'static {
|
||||
// This trait is intentionally left empty, by virtue of being a marker trait.
|
||||
//
|
||||
// Use additional traits with blanket implementations to attach functionality
|
||||
// to types that implement `Global`.
|
||||
}
|
||||
|
||||
/// A trait for reading a global value from the context.
|
||||
pub trait ReadGlobal {
|
||||
/// Returns the global instance of the implementing type.
|
||||
///
|
||||
/// Panics if a global for that type has not been assigned.
|
||||
fn global(cx: &AppContext) -> &Self;
|
||||
}
|
||||
|
||||
impl<T: Global> ReadGlobal for T {
|
||||
fn global(cx: &AppContext) -> &Self {
|
||||
cx.global::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for updating a global value in the context.
|
||||
pub trait UpdateGlobal {
|
||||
/// Updates the global instance of the implementing type using the provided closure.
|
||||
///
|
||||
/// This method provides the closure with mutable access to the context and the global simultaneously.
|
||||
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
F: FnOnce(&mut Self, &mut C) -> R;
|
||||
}
|
||||
|
||||
impl<T: Global> UpdateGlobal for T {
|
||||
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
F: FnOnce(&mut Self, &mut C) -> R,
|
||||
{
|
||||
cx.update_global(update)
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@ mod element;
|
||||
mod elements;
|
||||
mod executor;
|
||||
mod geometry;
|
||||
mod global;
|
||||
mod input;
|
||||
mod interactive;
|
||||
mod key_dispatch;
|
||||
@@ -125,6 +126,7 @@ pub use element::*;
|
||||
pub use elements::*;
|
||||
pub use executor::*;
|
||||
pub use geometry::*;
|
||||
pub use global::*;
|
||||
pub use gpui_macros::{register_action, test, IntoElement, Render};
|
||||
pub use input::*;
|
||||
pub use interactive::*;
|
||||
@@ -327,15 +329,3 @@ impl<T> Flatten<T> for Result<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A marker trait for types that can be stored in GPUI's global state.
|
||||
///
|
||||
/// This trait exists to provide type-safe access to globals by restricting
|
||||
/// the scope from which they can be accessed. For instance, the actual type
|
||||
/// that implements [`Global`] can be private, with public accessor functions
|
||||
/// that enforce correct usage.
|
||||
///
|
||||
/// Implement this on types you want to store in the context as a global.
|
||||
pub trait Global: 'static {
|
||||
// This trait is intentionally left empty, by virtue of being a marker trait.
|
||||
}
|
||||
|
||||
@@ -440,8 +440,8 @@ impl PlatformInput {
|
||||
mod test {
|
||||
|
||||
use crate::{
|
||||
self as gpui, div, Element, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
Keystroke, ParentElement, Render, TestAppContext, VisualContext,
|
||||
self as gpui, div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, Keystroke,
|
||||
ParentElement, Render, TestAppContext, VisualContext,
|
||||
};
|
||||
|
||||
struct TestView {
|
||||
@@ -453,7 +453,7 @@ mod test {
|
||||
actions!(test, [TestAction]);
|
||||
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl Element {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
div().id("testview").child(
|
||||
div()
|
||||
.key_context("parent")
|
||||
|
||||
@@ -108,6 +108,9 @@ pub(crate) trait Platform: 'static {
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn can_open_windows(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
@@ -132,6 +135,7 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
@@ -147,8 +151,10 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
|
||||
let underline = b_underlines[input.underline_id];
|
||||
if ((underline.wavy & 0xFFu) == 0u)
|
||||
{
|
||||
return vec4<f32>(0.0);
|
||||
return blend_color(input.color, input.color.a);
|
||||
}
|
||||
|
||||
let half_thickness = underline.thickness * 0.5;
|
||||
@@ -497,7 +497,7 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
|
||||
let distance_from_top_border = distance_in_pixels - half_thickness;
|
||||
let distance_from_bottom_border = distance_in_pixels + half_thickness;
|
||||
let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
|
||||
return blend_color(input.color, alpha);
|
||||
return blend_color(input.color, alpha * input.color.a);
|
||||
}
|
||||
|
||||
// --- monochrome sprites --- //
|
||||
|
||||
@@ -68,6 +68,10 @@ impl LinuxClient for HeadlessClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn can_open_windows(&self) -> anyhow::Result<()> {
|
||||
return Err(anyhow::anyhow!("neither DISPLAY, nor WAYLAND_DISPLAY found. You can still run zed for remote development with --dev-server-token."));
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
|
||||
@@ -34,7 +34,7 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
use crate::platform::linux::wayland::WaylandClient;
|
||||
use crate::{
|
||||
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
|
||||
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, Modifiers,
|
||||
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
|
||||
PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
|
||||
WindowOptions, WindowParams,
|
||||
@@ -55,6 +55,9 @@ pub trait LinuxClient {
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn can_open_windows(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
@@ -132,6 +135,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
});
|
||||
}
|
||||
|
||||
fn can_open_windows(&self) -> anyhow::Result<()> {
|
||||
self.can_open_windows()
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.with_common(|common| common.signal.stop());
|
||||
}
|
||||
@@ -368,6 +375,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
|
||||
// todo(linux)
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
@@ -485,7 +493,7 @@ pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
|
||||
if let Some(token) = activation_token {
|
||||
command.env("XDG_ACTIVATION_TOKEN", token);
|
||||
}
|
||||
match command.status() {
|
||||
match command.spawn() {
|
||||
Ok(_) => return,
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
@@ -655,6 +663,68 @@ impl Keystroke {
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which symbol the dead key represents
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux
|
||||
*/
|
||||
pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
|
||||
match keysym {
|
||||
Keysym::dead_grave => Some("`".to_owned()),
|
||||
Keysym::dead_acute => Some("´".to_owned()),
|
||||
Keysym::dead_circumflex => Some("^".to_owned()),
|
||||
Keysym::dead_tilde => Some("~".to_owned()),
|
||||
Keysym::dead_perispomeni => Some("͂".to_owned()),
|
||||
Keysym::dead_macron => Some("¯".to_owned()),
|
||||
Keysym::dead_breve => Some("˘".to_owned()),
|
||||
Keysym::dead_abovedot => Some("˙".to_owned()),
|
||||
Keysym::dead_diaeresis => Some("¨".to_owned()),
|
||||
Keysym::dead_abovering => Some("˚".to_owned()),
|
||||
Keysym::dead_doubleacute => Some("˝".to_owned()),
|
||||
Keysym::dead_caron => Some("ˇ".to_owned()),
|
||||
Keysym::dead_cedilla => Some("¸".to_owned()),
|
||||
Keysym::dead_ogonek => Some("˛".to_owned()),
|
||||
Keysym::dead_iota => Some("ͅ".to_owned()),
|
||||
Keysym::dead_voiced_sound => Some("゙".to_owned()),
|
||||
Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
|
||||
Keysym::dead_belowdot => Some("̣̣".to_owned()),
|
||||
Keysym::dead_hook => Some("̡".to_owned()),
|
||||
Keysym::dead_horn => Some("̛".to_owned()),
|
||||
Keysym::dead_stroke => Some("̶̶".to_owned()),
|
||||
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
|
||||
Keysym::dead_psili => Some("᾿".to_owned()),
|
||||
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
|
||||
Keysym::dead_dasia => Some("῾".to_owned()),
|
||||
Keysym::dead_doublegrave => Some("̏".to_owned()),
|
||||
Keysym::dead_belowring => Some("˳".to_owned()),
|
||||
Keysym::dead_belowmacron => Some("̱".to_owned()),
|
||||
Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
|
||||
Keysym::dead_belowtilde => Some("̰".to_owned()),
|
||||
Keysym::dead_belowbreve => Some("̮".to_owned()),
|
||||
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
|
||||
Keysym::dead_invertedbreve => Some("̯".to_owned()),
|
||||
Keysym::dead_belowcomma => Some("̦".to_owned()),
|
||||
Keysym::dead_currency => None,
|
||||
Keysym::dead_lowline => None,
|
||||
Keysym::dead_aboveverticalline => None,
|
||||
Keysym::dead_belowverticalline => None,
|
||||
Keysym::dead_longsolidusoverlay => None,
|
||||
Keysym::dead_a => None,
|
||||
Keysym::dead_A => None,
|
||||
Keysym::dead_e => None,
|
||||
Keysym::dead_E => None,
|
||||
Keysym::dead_i => None,
|
||||
Keysym::dead_I => None,
|
||||
Keysym::dead_o => None,
|
||||
Keysym::dead_O => None,
|
||||
Keysym::dead_u => None,
|
||||
Keysym::dead_U => None,
|
||||
Keysym::dead_small_schwa => Some("ə".to_owned()),
|
||||
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
|
||||
Keysym::dead_greek => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use core::hash;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::ffi::OsString;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
@@ -22,7 +23,7 @@ use wayland_client::event_created_child;
|
||||
use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContents};
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_pointer::{AxisRelativeDirection, AxisSource};
|
||||
use wayland_client::protocol::wl_pointer::AxisSource;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
@@ -42,6 +43,12 @@ use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
|
||||
};
|
||||
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::{
|
||||
ContentHint, ContentPurpose,
|
||||
};
|
||||
use wayland_protocols::wp::text_input::zv3::client::{
|
||||
zwp_text_input_manager_v3, zwp_text_input_v3,
|
||||
};
|
||||
use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};
|
||||
use wayland_protocols::xdg::activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1};
|
||||
use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
@@ -53,7 +60,7 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::window::{WaylandWindowState, WaylandWindowStatePtr};
|
||||
use super::window::{ImeInput, WaylandWindowState, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::cursor::Cursor;
|
||||
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
@@ -87,6 +94,7 @@ pub struct Globals {
|
||||
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
@@ -122,6 +130,7 @@ impl Globals {
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
executor,
|
||||
qh,
|
||||
}
|
||||
@@ -135,11 +144,14 @@ pub(crate) struct WaylandClientState {
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
|
||||
pre_edit_text: Option<String>,
|
||||
// Surface to Window mapping
|
||||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
// Output to scale mapping
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
drag: DragState,
|
||||
click: ClickState,
|
||||
repeat: KeyRepeat,
|
||||
@@ -241,6 +253,9 @@ impl Drop for WaylandClient {
|
||||
if let Some(data_device) = &state.data_device {
|
||||
data_device.release();
|
||||
}
|
||||
if let Some(text_input) = &state.text_input {
|
||||
text_input.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,10 +349,13 @@ impl WaylandClient {
|
||||
wl_pointer: None,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
text_input: None,
|
||||
pre_edit_text: None,
|
||||
output_scales: outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
keymap_state: None,
|
||||
compose_state: None,
|
||||
drag: DragState {
|
||||
data_offer: None,
|
||||
window: None,
|
||||
@@ -577,6 +595,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport);
|
||||
@@ -753,12 +772,17 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
capabilities: WEnum::Value(capabilities),
|
||||
} = event
|
||||
{
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
seat.get_keyboard(qh, ());
|
||||
state.text_input = state
|
||||
.globals
|
||||
.text_input_manager
|
||||
.as_ref()
|
||||
.map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let pointer = seat.get_pointer(qh, ());
|
||||
state.cursor_shape_device = state
|
||||
.globals
|
||||
@@ -798,9 +822,10 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
wl_keyboard::KeymapFormat::XkbV1,
|
||||
"Unsupported keymap format"
|
||||
);
|
||||
let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
let keymap = unsafe {
|
||||
xkb::Keymap::new_from_fd(
|
||||
&xkb::Context::new(xkb::CONTEXT_NO_FLAGS),
|
||||
&xkb_context,
|
||||
fd,
|
||||
size as usize,
|
||||
XKB_KEYMAP_FORMAT_TEXT_V1,
|
||||
@@ -810,7 +835,21 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
.flatten()
|
||||
.expect("Failed to create keymap")
|
||||
};
|
||||
let table = {
|
||||
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
|
||||
xkb::compose::Table::new_from_locale(
|
||||
&xkb_context,
|
||||
&locale,
|
||||
xkb::compose::COMPILE_NO_FLAGS,
|
||||
)
|
||||
.log_err()
|
||||
.unwrap()
|
||||
};
|
||||
state.keymap_state = Some(xkb::State::new(&keymap));
|
||||
state.compose_state = Some(xkb::compose::State::new(
|
||||
&table,
|
||||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
}
|
||||
wl_keyboard::Event::Enter { surface, .. } => {
|
||||
state.keyboard_focused_window = get_window(&mut state, &surface.id());
|
||||
@@ -827,7 +866,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state.enter_token.take();
|
||||
|
||||
if let Some(window) = keyboard_focused_window {
|
||||
if let Some(ref mut compose) = state.compose_state {
|
||||
compose.reset();
|
||||
}
|
||||
state.pre_edit_text.take();
|
||||
drop(state);
|
||||
window.handle_ime(ImeInput::DeleteText);
|
||||
window.set_focused(false);
|
||||
}
|
||||
}
|
||||
@@ -874,8 +918,47 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
|
||||
match key_state {
|
||||
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
|
||||
let mut keystroke =
|
||||
Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
|
||||
if let Some(mut compose) = state.compose_state.take() {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
focused_window.handle_ime(ImeInput::SetMarkedText(pre_edit));
|
||||
state = client.borrow_mut();
|
||||
}
|
||||
|
||||
xkb::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = compose.utf8();
|
||||
keystroke.key = xkb::keysym_get_name(compose.keysym().unwrap());
|
||||
}
|
||||
xkb::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
focused_window.handle_ime(ImeInput::InsertText(pre_edit));
|
||||
}
|
||||
if let Some(current_key) =
|
||||
Keystroke::underlying_dead_key(keysym)
|
||||
{
|
||||
focused_window
|
||||
.handle_ime(ImeInput::SetMarkedText(current_key));
|
||||
}
|
||||
compose.feed(keysym);
|
||||
state = client.borrow_mut();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
state.compose_state = Some(compose);
|
||||
}
|
||||
let input = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
|
||||
keystroke: keystroke,
|
||||
is_held: false, // todo(linux)
|
||||
});
|
||||
|
||||
@@ -932,6 +1015,86 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
text_input: &zwp_text_input_v3::ZwpTextInputV3,
|
||||
event: <zwp_text_input_v3::ZwpTextInputV3 as Proxy>::Event,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
match event {
|
||||
zwp_text_input_v3::Event::Enter { surface } => {
|
||||
text_input.enable();
|
||||
text_input.set_content_type(ContentHint::None, ContentPurpose::Normal);
|
||||
|
||||
if let Some(window) = state.keyboard_focused_window.clone() {
|
||||
drop(state);
|
||||
if let Some(area) = window.get_ime_area() {
|
||||
text_input.set_cursor_rectangle(
|
||||
area.origin.x.0 as i32,
|
||||
area.origin.y.0 as i32,
|
||||
area.size.width.0 as i32,
|
||||
area.size.height.0 as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
text_input.commit();
|
||||
}
|
||||
zwp_text_input_v3::Event::Leave { surface } => {
|
||||
text_input.disable();
|
||||
text_input.commit();
|
||||
}
|
||||
zwp_text_input_v3::Event::CommitString { text } => {
|
||||
let Some(window) = state.keyboard_focused_window.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(commit_text) = text {
|
||||
drop(state);
|
||||
window.handle_ime(ImeInput::InsertText(commit_text));
|
||||
}
|
||||
}
|
||||
zwp_text_input_v3::Event::PreeditString {
|
||||
text,
|
||||
cursor_begin,
|
||||
cursor_end,
|
||||
} => {
|
||||
state.pre_edit_text = text;
|
||||
}
|
||||
zwp_text_input_v3::Event::Done { serial } => {
|
||||
let last_serial = state.serial_tracker.get(SerialKind::InputMethod);
|
||||
state.serial_tracker.update(SerialKind::InputMethod, serial);
|
||||
let Some(window) = state.keyboard_focused_window.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(text) = state.pre_edit_text.take() {
|
||||
drop(state);
|
||||
window.handle_ime(ImeInput::SetMarkedText(text));
|
||||
if let Some(area) = window.get_ime_area() {
|
||||
text_input.set_cursor_rectangle(
|
||||
area.origin.x.0 as i32,
|
||||
area.origin.y.0 as i32,
|
||||
area.size.width.0 as i32,
|
||||
area.size.height.0 as i32,
|
||||
);
|
||||
if last_serial == serial {
|
||||
text_input.commit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drop(state);
|
||||
window.handle_ime(ImeInput::DeleteText);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
|
||||
// These values are coming from <linux/input-event-codes.h>.
|
||||
@@ -1053,6 +1216,16 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
match button_state {
|
||||
wl_pointer::ButtonState::Pressed => {
|
||||
if let (Some(window), Some(text), Some(compose_state)) = (
|
||||
state.keyboard_focused_window.clone(),
|
||||
state.pre_edit_text.take(),
|
||||
state.compose_state.as_mut(),
|
||||
) {
|
||||
compose_state.reset();
|
||||
drop(state);
|
||||
window.handle_ime(ImeInput::InsertText(text));
|
||||
state = client.borrow_mut();
|
||||
}
|
||||
let click_elapsed = state.click.last_click.elapsed();
|
||||
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
@@ -1161,24 +1334,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::AxisRelativeDirection {
|
||||
axis: WEnum::Value(axis),
|
||||
direction: WEnum::Value(direction),
|
||||
} => match (axis, direction) {
|
||||
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Identical) => {
|
||||
state.vertical_modifier = -1.0
|
||||
}
|
||||
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Inverted) => {
|
||||
state.vertical_modifier = 1.0
|
||||
}
|
||||
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Identical) => {
|
||||
state.horizontal_modifier = -1.0
|
||||
}
|
||||
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Inverted) => {
|
||||
state.horizontal_modifier = 1.0
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
wl_pointer::Event::AxisValue120 {
|
||||
axis: WEnum::Value(axis),
|
||||
value120,
|
||||
|
||||
@@ -5,6 +5,7 @@ use collections::HashMap;
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum SerialKind {
|
||||
DataDevice,
|
||||
InputMethod,
|
||||
MouseEnter,
|
||||
MousePress,
|
||||
KeyPress,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::any::Any;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::ffi::c_void;
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::Range;
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
@@ -162,6 +163,11 @@ impl WaylandWindowState {
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
|
||||
pub enum ImeInput {
|
||||
InsertText(String),
|
||||
SetMarkedText(String),
|
||||
DeleteText,
|
||||
}
|
||||
|
||||
impl Drop for WaylandWindow {
|
||||
fn drop(&mut self) {
|
||||
@@ -425,6 +431,40 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_ime(&self, ime: ImeInput) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
match ime {
|
||||
ImeInput::InsertText(text) => {
|
||||
input_handler.replace_text_in_range(None, &text);
|
||||
}
|
||||
ImeInput::SetMarkedText(text) => {
|
||||
input_handler.replace_and_mark_text_in_range(None, &text, None);
|
||||
}
|
||||
ImeInput::DeleteText => {
|
||||
if let Some(marked) = input_handler.marked_text_range() {
|
||||
input_handler.replace_text_in_range(Some(marked), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ime_area(&self) -> Option<Bounds<Pixels>> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let mut bounds: Option<Bounds<Pixels>> = None;
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
if let Some(range) = input_handler.selected_text_range() {
|
||||
bounds = input_handler.bounds_for_range(range);
|
||||
}
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn set_size_and_scale(
|
||||
&self,
|
||||
width: Option<NonZeroU32>,
|
||||
|
||||
@@ -2,8 +2,10 @@ mod client;
|
||||
mod display;
|
||||
mod event;
|
||||
mod window;
|
||||
mod xim_handler;
|
||||
|
||||
pub(crate) use client::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use event::*;
|
||||
pub(crate) use window::*;
|
||||
pub(crate) use xim_handler::*;
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
use calloop::{channel, EventLoop, LoopHandle, RegistrationToken};
|
||||
|
||||
use collections::HashMap;
|
||||
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
|
||||
use copypasta::ClipboardProvider;
|
||||
@@ -20,6 +21,7 @@ use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xim::{x11rb::X11rbClient, Client};
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
|
||||
@@ -36,6 +38,7 @@ use super::{
|
||||
X11Display, X11WindowStatePtr, XcbAtoms,
|
||||
};
|
||||
use super::{button_from_mask, button_of_key, modifiers_from_state};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
|
||||
@@ -52,6 +55,36 @@ impl Deref for WindowRef {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum EventHandlerError {
|
||||
XCBConnectionError(ConnectionError),
|
||||
XIMClientError(xim::ClientError),
|
||||
}
|
||||
|
||||
impl std::error::Error for EventHandlerError {}
|
||||
|
||||
impl std::fmt::Display for EventHandlerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EventHandlerError::XCBConnectionError(err) => err.fmt(f),
|
||||
EventHandlerError::XIMClientError(err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConnectionError> for EventHandlerError {
|
||||
fn from(err: ConnectionError) -> Self {
|
||||
EventHandlerError::XCBConnectionError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<xim::ClientError> for EventHandlerError {
|
||||
fn from(err: xim::ClientError) -> Self {
|
||||
EventHandlerError::XIMClientError(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
@@ -69,6 +102,8 @@ pub struct X11ClientState {
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
@@ -227,12 +262,21 @@ impl X11Client {
|
||||
|
||||
let xcb_connection = Rc::new(xcb_connection);
|
||||
|
||||
let (xim_tx, xim_rx) = channel::channel::<XimCallbackEvent>();
|
||||
|
||||
let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok();
|
||||
let xim_handler = if ximc.is_some() {
|
||||
Some(XimHandler::new(xim_tx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<ConnectionError>(
|
||||
Generic::new_with_error::<EventHandlerError>(
|
||||
fd,
|
||||
calloop::Interest::READ,
|
||||
calloop::Mode::Level,
|
||||
@@ -241,14 +285,63 @@ impl X11Client {
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
while let Some(event) = xcb_connection.poll_for_event()? {
|
||||
client.handle_event(event);
|
||||
let mut state = client.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
client.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let mut state = client.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
if xim_connected {
|
||||
client.xim_handle_event(event);
|
||||
} else {
|
||||
client.handle_event(event);
|
||||
}
|
||||
}
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
handle
|
||||
.insert_source(xim_rx, {
|
||||
move |chan_event, _, client| match chan_event {
|
||||
channel::Event::Msg(xim_event) => {
|
||||
match (xim_event) {
|
||||
XimCallbackEvent::XimXEvent(event) => {
|
||||
client.handle_event(event);
|
||||
}
|
||||
XimCallbackEvent::XimCommitEvent(window, text) => {
|
||||
client.xim_handle_commit(window, text);
|
||||
}
|
||||
XimCallbackEvent::XimPreeditEvent(window, text) => {
|
||||
client.xim_handle_preedit(window, text);
|
||||
}
|
||||
};
|
||||
}
|
||||
channel::Event::Closed => {
|
||||
log::error!("XIM Event Sender dropped")
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize XIM event source");
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
@@ -265,6 +358,8 @@ impl X11Client {
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
xkb: xkb_state,
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
cursor_handle,
|
||||
cursor_styles: HashMap::default(),
|
||||
@@ -365,7 +460,6 @@ impl X11Client {
|
||||
}
|
||||
keystroke
|
||||
};
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
@@ -550,6 +644,79 @@ impl X11Client {
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn xim_handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::KeyPress(event) | Event::KeyRelease(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
drop(state);
|
||||
xim_handler.window = event.event;
|
||||
ximc.forward_event(
|
||||
xim_handler.im_id,
|
||||
xim_handler.ic_id,
|
||||
xim::ForwardEventFlag::empty(),
|
||||
&event,
|
||||
)
|
||||
.unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
}
|
||||
event => {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
|
||||
window.handle_ime_commit(text);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
window.handle_ime_preedit(text);
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
drop(state);
|
||||
|
||||
if let Some(area) = window.get_ime_area() {
|
||||
let ic_attributes = ximc
|
||||
.build_ic_attributes()
|
||||
.push(
|
||||
xim::AttributeName::InputStyle,
|
||||
xim::InputStyle::PREEDIT_CALLBACKS
|
||||
| xim::InputStyle::STATUS_NOTHING
|
||||
| xim::InputStyle::PREEDIT_POSITION,
|
||||
)
|
||||
.push(xim::AttributeName::ClientWindow, xim_handler.window)
|
||||
.push(xim::AttributeName::FocusWindow, xim_handler.window)
|
||||
.nested_list(xim::AttributeName::PreeditAttributes, |b| {
|
||||
b.push(
|
||||
xim::AttributeName::SpotLocation,
|
||||
xim::Point {
|
||||
x: u32::from(area.origin.x + area.size.width) as i16,
|
||||
y: u32::from(area.origin.y + area.size.height) as i16,
|
||||
},
|
||||
);
|
||||
})
|
||||
.build();
|
||||
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes);
|
||||
}
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClient for X11Client {
|
||||
|
||||
@@ -478,6 +478,40 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_ime_commit(&self, text: String) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, &text);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_ime_preedit(&self, text: String) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
input_handler.replace_and_mark_text_in_range(None, &text, None);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ime_area(&self) -> Option<Bounds<Pixels>> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let mut bounds: Option<Bounds<Pixels>> = None;
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
drop(state);
|
||||
if let Some(range) = input_handler.selected_text_range() {
|
||||
bounds = input_handler.bounds_for_range(range);
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.input_handler = Some(input_handler);
|
||||
};
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn configure(&self, bounds: Bounds<i32>) {
|
||||
let mut resize_args = None;
|
||||
let do_move;
|
||||
|
||||
154
crates/gpui/src/platform/linux/x11/xim_handler.rs
Normal file
154
crates/gpui/src/platform/linux/x11/xim_handler.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::cell::RefCell;
|
||||
use std::default::Default;
|
||||
use std::rc::Rc;
|
||||
|
||||
use calloop::channel;
|
||||
|
||||
use x11rb::protocol::{xproto, Event};
|
||||
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle, Point};
|
||||
|
||||
use crate::{Keystroke, PlatformInput, X11ClientState};
|
||||
|
||||
pub enum XimCallbackEvent {
|
||||
XimXEvent(x11rb::protocol::Event),
|
||||
XimPreeditEvent(xproto::Window, String),
|
||||
XimCommitEvent(xproto::Window, String),
|
||||
}
|
||||
|
||||
pub struct XimHandler {
|
||||
pub im_id: u16,
|
||||
pub ic_id: u16,
|
||||
pub xim_tx: channel::Sender<XimCallbackEvent>,
|
||||
pub connected: bool,
|
||||
pub window: xproto::Window,
|
||||
}
|
||||
|
||||
impl XimHandler {
|
||||
pub fn new(xim_tx: channel::Sender<XimCallbackEvent>) -> Self {
|
||||
Self {
|
||||
im_id: Default::default(),
|
||||
ic_id: Default::default(),
|
||||
xim_tx,
|
||||
connected: false,
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler {
|
||||
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
|
||||
client.open("C")
|
||||
}
|
||||
|
||||
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
|
||||
self.im_id = input_method_id;
|
||||
|
||||
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
|
||||
}
|
||||
|
||||
fn handle_get_im_values(
|
||||
&mut self,
|
||||
client: &mut C,
|
||||
input_method_id: u16,
|
||||
_attributes: AHashMap<AttributeName, Vec<u8>>,
|
||||
) -> Result<(), ClientError> {
|
||||
let ic_attributes = client
|
||||
.build_ic_attributes()
|
||||
.push(
|
||||
AttributeName::InputStyle,
|
||||
InputStyle::PREEDIT_CALLBACKS
|
||||
| InputStyle::STATUS_NOTHING
|
||||
| InputStyle::PREEDIT_NONE,
|
||||
)
|
||||
.push(AttributeName::ClientWindow, self.window)
|
||||
.push(AttributeName::FocusWindow, self.window)
|
||||
.build();
|
||||
client.create_ic(input_method_id, ic_attributes)
|
||||
}
|
||||
|
||||
fn handle_create_ic(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
input_context_id: u16,
|
||||
) -> Result<(), ClientError> {
|
||||
self.connected = true;
|
||||
self.ic_id = input_context_id;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_commit(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
text: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
self.xim_tx.send(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_forward_event(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
_flag: xim::ForwardEventFlag,
|
||||
xev: C::XEvent,
|
||||
) -> Result<(), ClientError> {
|
||||
match (xev.response_type) {
|
||||
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
|
||||
}
|
||||
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> {
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
fn handle_destroy_ic(
|
||||
&mut self,
|
||||
client: &mut C,
|
||||
input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
) -> Result<(), ClientError> {
|
||||
client.close(input_method_id)
|
||||
}
|
||||
|
||||
fn handle_preedit_draw(
|
||||
&mut self,
|
||||
_client: &mut C,
|
||||
_input_method_id: u16,
|
||||
_input_context_id: u16,
|
||||
_caret: i32,
|
||||
_chg_first: i32,
|
||||
_chg_len: i32,
|
||||
_status: xim::PreeditDrawStatus,
|
||||
preedit_string: &str,
|
||||
_feedbacks: Vec<xim::Feedback>,
|
||||
) -> Result<(), ClientError> {
|
||||
// XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text
|
||||
// XIMUnderline: 2, XIMSecondary: 16: underlined text
|
||||
// XIMHighlight: 4: normal text
|
||||
// XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position
|
||||
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
|
||||
// but interchangeable as above
|
||||
// Currently there's no way to support these.
|
||||
let mark_range = self.xim_tx.send(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::{
|
||||
Hsla, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
|
||||
ScaledPixels, Scene, Shadow, Size, Surface, Underline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
base::{NO, YES},
|
||||
@@ -27,9 +28,8 @@ pub(crate) type PointF = crate::Point<f32>;
|
||||
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
|
||||
#[cfg(feature = "runtime_shaders")]
|
||||
const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
|
||||
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 type Context = Arc<Mutex<Vec<metal::Buffer>>>;
|
||||
pub type Context = Arc<Mutex<InstanceBufferPool>>;
|
||||
pub type Renderer = MetalRenderer;
|
||||
|
||||
pub unsafe fn new_renderer(
|
||||
@@ -42,6 +42,51 @@ pub unsafe fn new_renderer(
|
||||
MetalRenderer::new(context)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
buffer_size: usize,
|
||||
buffers: Vec<metal::Buffer>,
|
||||
}
|
||||
|
||||
impl Default for InstanceBufferPool {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer_size: 2 * 1024 * 1024,
|
||||
buffers: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBuffer {
|
||||
metal_buffer: metal::Buffer,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl InstanceBufferPool {
|
||||
pub(crate) fn reset(&mut self, buffer_size: usize) {
|
||||
self.buffer_size = buffer_size;
|
||||
self.buffers.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn acquire(&mut self, device: &metal::Device) -> InstanceBuffer {
|
||||
let buffer = self.buffers.pop().unwrap_or_else(|| {
|
||||
device.new_buffer(
|
||||
self.buffer_size as u64,
|
||||
MTLResourceOptions::StorageModeManaged,
|
||||
)
|
||||
});
|
||||
InstanceBuffer {
|
||||
metal_buffer: buffer,
|
||||
size: self.buffer_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn release(&mut self, buffer: InstanceBuffer) {
|
||||
if buffer.size == self.buffer_size {
|
||||
self.buffers.push(buffer.metal_buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MetalRenderer {
|
||||
device: metal::Device,
|
||||
layer: metal::MetalLayer,
|
||||
@@ -57,13 +102,13 @@ pub(crate) struct MetalRenderer {
|
||||
surfaces_pipeline_state: metal::RenderPipelineState,
|
||||
unit_vertices: metal::Buffer,
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
|
||||
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
|
||||
sprite_atlas: Arc<MetalAtlas>,
|
||||
core_video_texture_cache: CVMetalTextureCache,
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
|
||||
device
|
||||
} else {
|
||||
@@ -256,24 +301,74 @@ 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,
|
||||
)
|
||||
});
|
||||
|
||||
loop {
|
||||
let mut instance_buffer = self.instance_buffer_pool.lock().acquire(&self.device);
|
||||
|
||||
let command_buffer =
|
||||
self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size);
|
||||
|
||||
match command_buffer {
|
||||
Ok(command_buffer) => {
|
||||
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().release(instance_buffer);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
command_buffer.add_completed_handler(&block);
|
||||
|
||||
if self.presents_with_transaction {
|
||||
command_buffer.commit();
|
||||
command_buffer.wait_until_scheduled();
|
||||
drawable.present();
|
||||
} else {
|
||||
command_buffer.present_drawable(drawable);
|
||||
command_buffer.commit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"failed to render: {}. retrying with larger instance buffer size",
|
||||
err
|
||||
);
|
||||
let mut instance_buffer_pool = self.instance_buffer_pool.lock();
|
||||
let buffer_size = instance_buffer_pool.buffer_size;
|
||||
if buffer_size >= 256 * 1024 * 1024 {
|
||||
log::error!("instance buffer size grew too large: {}", buffer_size);
|
||||
break;
|
||||
}
|
||||
instance_buffer_pool.reset(buffer_size * 2);
|
||||
log::info!(
|
||||
"increased instance buffer size to {}",
|
||||
instance_buffer_pool.buffer_size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_primitives(
|
||||
&mut self,
|
||||
scene: &Scene,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
drawable: &metal::MetalDrawableRef,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
) -> Result<metal::CommandBuffer> {
|
||||
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_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
command_buffer,
|
||||
) else {
|
||||
log::error!("failed to rasterize {} paths", scene.paths().len());
|
||||
return;
|
||||
return Err(anyhow!("failed to rasterize {} paths", scene.paths().len()));
|
||||
};
|
||||
|
||||
let render_pass_descriptor = metal::RenderPassDescriptor::new();
|
||||
@@ -302,14 +397,14 @@ impl MetalRenderer {
|
||||
let ok = match batch {
|
||||
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
|
||||
shadows,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Quads(quads) => self.draw_quads(
|
||||
quads,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -317,14 +412,14 @@ impl MetalRenderer {
|
||||
PrimitiveBatch::Paths(paths) => self.draw_paths(
|
||||
paths,
|
||||
&path_tiles,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
|
||||
underlines,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -335,7 +430,7 @@ impl MetalRenderer {
|
||||
} => self.draw_monochrome_sprites(
|
||||
texture_id,
|
||||
sprites,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -346,14 +441,14 @@ impl MetalRenderer {
|
||||
} => self.draw_polychrome_sprites(
|
||||
texture_id,
|
||||
sprites,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
),
|
||||
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
|
||||
surfaces,
|
||||
&mut instance_buffer,
|
||||
instance_buffer,
|
||||
&mut instance_offset,
|
||||
viewport_size,
|
||||
command_encoder,
|
||||
@@ -361,7 +456,8 @@ impl MetalRenderer {
|
||||
};
|
||||
|
||||
if !ok {
|
||||
log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
command_encoder.end_encoding();
|
||||
return Err(anyhow!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
@@ -369,47 +465,28 @@ impl MetalRenderer {
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),
|
||||
);
|
||||
break;
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
command_encoder.end_encoding();
|
||||
|
||||
instance_buffer.did_modify_range(NSRange {
|
||||
instance_buffer.metal_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);
|
||||
|
||||
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
if self.presents_with_transaction {
|
||||
command_buffer.commit();
|
||||
command_buffer.wait_until_scheduled();
|
||||
drawable.present();
|
||||
} else {
|
||||
command_buffer.present_drawable(drawable);
|
||||
command_buffer.commit();
|
||||
}
|
||||
Ok(command_buffer.to_owned())
|
||||
}
|
||||
|
||||
fn rasterize_paths(
|
||||
&mut self,
|
||||
paths: &[Path<ScaledPixels>],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
command_buffer: &metal::CommandBufferRef,
|
||||
) -> Option<HashMap<PathId, AtlasTile>> {
|
||||
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
let mut tiles = HashMap::default();
|
||||
let mut vertices_by_texture_id = HashMap::default();
|
||||
for path in paths {
|
||||
@@ -436,7 +513,7 @@ impl MetalRenderer {
|
||||
align_offset(instance_offset);
|
||||
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
|
||||
let next_offset = *instance_offset + vertices_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -455,7 +532,7 @@ impl MetalRenderer {
|
||||
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
|
||||
command_encoder.set_vertex_buffer(
|
||||
PathRasterizationInputIndex::Vertices as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
let texture_size = Size {
|
||||
@@ -468,8 +545,9 @@ impl MetalRenderer {
|
||||
&texture_size as *const Size<DevicePixels> as *const _,
|
||||
);
|
||||
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
let buffer_contents = unsafe {
|
||||
(instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset)
|
||||
};
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
vertices.as_ptr() as *const u8,
|
||||
@@ -493,7 +571,7 @@ impl MetalRenderer {
|
||||
fn draw_shadows(
|
||||
&mut self,
|
||||
shadows: &[Shadow],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -511,12 +589,12 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
ShadowInputIndex::Shadows as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
ShadowInputIndex::Shadows as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
@@ -528,10 +606,10 @@ impl MetalRenderer {
|
||||
|
||||
let shadow_bytes_len = mem::size_of_val(shadows);
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + shadow_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -556,7 +634,7 @@ impl MetalRenderer {
|
||||
fn draw_quads(
|
||||
&mut self,
|
||||
quads: &[Quad],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -574,12 +652,12 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
QuadInputIndex::Quads as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
QuadInputIndex::Quads as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
@@ -591,10 +669,10 @@ impl MetalRenderer {
|
||||
|
||||
let quad_bytes_len = mem::size_of_val(quads);
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + quad_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -616,7 +694,7 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
paths: &[Path<ScaledPixels>],
|
||||
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -675,7 +753,7 @@ impl MetalRenderer {
|
||||
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -685,7 +763,7 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder
|
||||
@@ -693,12 +771,13 @@ impl MetalRenderer {
|
||||
|
||||
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
let buffer_contents = unsafe {
|
||||
(instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset)
|
||||
};
|
||||
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
@@ -724,7 +803,7 @@ impl MetalRenderer {
|
||||
fn draw_underlines(
|
||||
&mut self,
|
||||
underlines: &[Underline],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -742,12 +821,12 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
UnderlineInputIndex::Underlines as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
UnderlineInputIndex::Underlines as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
|
||||
@@ -759,10 +838,10 @@ impl MetalRenderer {
|
||||
|
||||
let underline_bytes_len = mem::size_of_val(underlines);
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + underline_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -788,7 +867,7 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
texture_id: AtlasTextureId,
|
||||
sprites: &[MonochromeSprite],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -798,6 +877,15 @@ impl MetalRenderer {
|
||||
}
|
||||
align_offset(instance_offset);
|
||||
|
||||
let sprite_bytes_len = mem::size_of_val(sprites);
|
||||
let buffer_contents =
|
||||
unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
let texture = self.sprite_atlas.metal_texture(texture_id);
|
||||
let texture_size = size(
|
||||
DevicePixels(texture.width() as i32),
|
||||
@@ -811,7 +899,7 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -826,20 +914,11 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_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 { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
sprites.as_ptr() as *const u8,
|
||||
@@ -862,7 +941,7 @@ impl MetalRenderer {
|
||||
&mut self,
|
||||
texture_id: AtlasTextureId,
|
||||
sprites: &[PolychromeSprite],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -885,7 +964,7 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_vertex_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -900,17 +979,17 @@ impl MetalRenderer {
|
||||
);
|
||||
command_encoder.set_fragment_buffer(
|
||||
SpriteInputIndex::Sprites as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_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 { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
unsafe { (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) };
|
||||
|
||||
let next_offset = *instance_offset + sprite_bytes_len;
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -935,7 +1014,7 @@ impl MetalRenderer {
|
||||
fn draw_surfaces(
|
||||
&mut self,
|
||||
surfaces: &[Surface],
|
||||
instance_buffer: &mut metal::Buffer,
|
||||
instance_buffer: &mut InstanceBuffer,
|
||||
instance_offset: &mut usize,
|
||||
viewport_size: Size<DevicePixels>,
|
||||
command_encoder: &metal::RenderCommandEncoderRef,
|
||||
@@ -990,13 +1069,13 @@ impl MetalRenderer {
|
||||
|
||||
align_offset(instance_offset);
|
||||
let next_offset = *instance_offset + mem::size_of::<Surface>();
|
||||
if next_offset > INSTANCE_BUFFER_SIZE {
|
||||
if next_offset > instance_buffer.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
command_encoder.set_vertex_buffer(
|
||||
SurfaceInputIndex::Surfaces as u64,
|
||||
Some(instance_buffer),
|
||||
Some(&instance_buffer.metal_buffer),
|
||||
*instance_offset as u64,
|
||||
);
|
||||
command_encoder.set_vertex_bytes(
|
||||
@@ -1014,7 +1093,8 @@ impl MetalRenderer {
|
||||
);
|
||||
|
||||
unsafe {
|
||||
let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
|
||||
let buffer_contents = (instance_buffer.metal_buffer.contents() as *mut u8)
|
||||
.add(*instance_offset)
|
||||
as *mut SurfaceBounds;
|
||||
ptr::write(
|
||||
buffer_contents,
|
||||
|
||||
@@ -2,389 +2,80 @@
|
||||
|
||||
use crate::FontFeatures;
|
||||
use cocoa::appkit::CGFloat;
|
||||
use core_foundation::{base::TCFType, number::CFNumber};
|
||||
use core_graphics::geometry::CGAffineTransform;
|
||||
use core_foundation::{
|
||||
array::{
|
||||
kCFTypeArrayCallBacks, CFArray, CFArrayAppendValue, CFArrayCreateMutable, CFMutableArrayRef,
|
||||
},
|
||||
base::{kCFAllocatorDefault, CFRelease, TCFType},
|
||||
dictionary::{
|
||||
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, CFDictionaryCreate,
|
||||
},
|
||||
number::CFNumber,
|
||||
string::{CFString, CFStringRef},
|
||||
};
|
||||
use core_graphics::{display::CFDictionary, geometry::CGAffineTransform};
|
||||
use core_text::{
|
||||
font::{CTFont, CTFontRef},
|
||||
font_descriptor::{
|
||||
CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
|
||||
kCTFontFeatureSettingsAttribute, CTFontDescriptor, CTFontDescriptorCopyAttributes,
|
||||
CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorCreateWithAttributes,
|
||||
CTFontDescriptorRef,
|
||||
},
|
||||
};
|
||||
use font_kit::font::Font;
|
||||
use std::ptr;
|
||||
|
||||
const kCaseSensitiveLayoutOffSelector: i32 = 1;
|
||||
const kCaseSensitiveLayoutOnSelector: i32 = 0;
|
||||
const kCaseSensitiveLayoutType: i32 = 33;
|
||||
const kCaseSensitiveSpacingOffSelector: i32 = 3;
|
||||
const kCaseSensitiveSpacingOnSelector: i32 = 2;
|
||||
const kCharacterAlternativesType: i32 = 17;
|
||||
const kCommonLigaturesOffSelector: i32 = 3;
|
||||
const kCommonLigaturesOnSelector: i32 = 2;
|
||||
const kContextualAlternatesOffSelector: i32 = 1;
|
||||
const kContextualAlternatesOnSelector: i32 = 0;
|
||||
const kContextualAlternatesType: i32 = 36;
|
||||
const kContextualLigaturesOffSelector: i32 = 19;
|
||||
const kContextualLigaturesOnSelector: i32 = 18;
|
||||
const kContextualSwashAlternatesOffSelector: i32 = 5;
|
||||
const kContextualSwashAlternatesOnSelector: i32 = 4;
|
||||
const kDefaultLowerCaseSelector: i32 = 0;
|
||||
const kDefaultUpperCaseSelector: i32 = 0;
|
||||
const kDiagonalFractionsSelector: i32 = 2;
|
||||
const kFractionsType: i32 = 11;
|
||||
const kHistoricalLigaturesOffSelector: i32 = 21;
|
||||
const kHistoricalLigaturesOnSelector: i32 = 20;
|
||||
const kHojoCharactersSelector: i32 = 12;
|
||||
const kInferiorsSelector: i32 = 2;
|
||||
const kJIS2004CharactersSelector: i32 = 11;
|
||||
const kLigaturesType: i32 = 1;
|
||||
const kLowerCasePetiteCapsSelector: i32 = 2;
|
||||
const kLowerCaseSmallCapsSelector: i32 = 1;
|
||||
const kLowerCaseType: i32 = 37;
|
||||
const kLowerCaseNumbersSelector: i32 = 0;
|
||||
const kMathematicalGreekOffSelector: i32 = 11;
|
||||
const kMathematicalGreekOnSelector: i32 = 10;
|
||||
const kMonospacedNumbersSelector: i32 = 0;
|
||||
const kNLCCharactersSelector: i32 = 13;
|
||||
const kNoFractionsSelector: i32 = 0;
|
||||
const kNormalPositionSelector: i32 = 0;
|
||||
const kNoStyleOptionsSelector: i32 = 0;
|
||||
const kNumberCaseType: i32 = 21;
|
||||
const kNumberSpacingType: i32 = 6;
|
||||
const kOrdinalsSelector: i32 = 3;
|
||||
const kProportionalNumbersSelector: i32 = 1;
|
||||
const kQuarterWidthTextSelector: i32 = 4;
|
||||
const kScientificInferiorsSelector: i32 = 4;
|
||||
const kSlashedZeroOffSelector: i32 = 5;
|
||||
const kSlashedZeroOnSelector: i32 = 4;
|
||||
const kStyleOptionsType: i32 = 19;
|
||||
const kStylisticAltEighteenOffSelector: i32 = 37;
|
||||
const kStylisticAltEighteenOnSelector: i32 = 36;
|
||||
const kStylisticAltEightOffSelector: i32 = 17;
|
||||
const kStylisticAltEightOnSelector: i32 = 16;
|
||||
const kStylisticAltElevenOffSelector: i32 = 23;
|
||||
const kStylisticAltElevenOnSelector: i32 = 22;
|
||||
const kStylisticAlternativesType: i32 = 35;
|
||||
const kStylisticAltFifteenOffSelector: i32 = 31;
|
||||
const kStylisticAltFifteenOnSelector: i32 = 30;
|
||||
const kStylisticAltFiveOffSelector: i32 = 11;
|
||||
const kStylisticAltFiveOnSelector: i32 = 10;
|
||||
const kStylisticAltFourOffSelector: i32 = 9;
|
||||
const kStylisticAltFourOnSelector: i32 = 8;
|
||||
const kStylisticAltFourteenOffSelector: i32 = 29;
|
||||
const kStylisticAltFourteenOnSelector: i32 = 28;
|
||||
const kStylisticAltNineOffSelector: i32 = 19;
|
||||
const kStylisticAltNineOnSelector: i32 = 18;
|
||||
const kStylisticAltNineteenOffSelector: i32 = 39;
|
||||
const kStylisticAltNineteenOnSelector: i32 = 38;
|
||||
const kStylisticAltOneOffSelector: i32 = 3;
|
||||
const kStylisticAltOneOnSelector: i32 = 2;
|
||||
const kStylisticAltSevenOffSelector: i32 = 15;
|
||||
const kStylisticAltSevenOnSelector: i32 = 14;
|
||||
const kStylisticAltSeventeenOffSelector: i32 = 35;
|
||||
const kStylisticAltSeventeenOnSelector: i32 = 34;
|
||||
const kStylisticAltSixOffSelector: i32 = 13;
|
||||
const kStylisticAltSixOnSelector: i32 = 12;
|
||||
const kStylisticAltSixteenOffSelector: i32 = 33;
|
||||
const kStylisticAltSixteenOnSelector: i32 = 32;
|
||||
const kStylisticAltTenOffSelector: i32 = 21;
|
||||
const kStylisticAltTenOnSelector: i32 = 20;
|
||||
const kStylisticAltThirteenOffSelector: i32 = 27;
|
||||
const kStylisticAltThirteenOnSelector: i32 = 26;
|
||||
const kStylisticAltThreeOffSelector: i32 = 7;
|
||||
const kStylisticAltThreeOnSelector: i32 = 6;
|
||||
const kStylisticAltTwelveOffSelector: i32 = 25;
|
||||
const kStylisticAltTwelveOnSelector: i32 = 24;
|
||||
const kStylisticAltTwentyOffSelector: i32 = 41;
|
||||
const kStylisticAltTwentyOnSelector: i32 = 40;
|
||||
const kStylisticAltTwoOffSelector: i32 = 5;
|
||||
const kStylisticAltTwoOnSelector: i32 = 4;
|
||||
const kSuperiorsSelector: i32 = 1;
|
||||
const kSwashAlternatesOffSelector: i32 = 3;
|
||||
const kSwashAlternatesOnSelector: i32 = 2;
|
||||
const kTitlingCapsSelector: i32 = 4;
|
||||
const kTypographicExtrasType: i32 = 14;
|
||||
const kVerticalFractionsSelector: i32 = 1;
|
||||
const kVerticalPositionType: i32 = 10;
|
||||
|
||||
pub fn apply_features(font: &mut Font, features: &FontFeatures) {
|
||||
// See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
|
||||
// for a reference implementation.
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.calt(),
|
||||
kContextualAlternatesType,
|
||||
kContextualAlternatesOnSelector,
|
||||
kContextualAlternatesOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.case(),
|
||||
kCaseSensitiveLayoutType,
|
||||
kCaseSensitiveLayoutOnSelector,
|
||||
kCaseSensitiveLayoutOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.cpsp(),
|
||||
kCaseSensitiveLayoutType,
|
||||
kCaseSensitiveSpacingOnSelector,
|
||||
kCaseSensitiveSpacingOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.frac(),
|
||||
kFractionsType,
|
||||
kDiagonalFractionsSelector,
|
||||
kNoFractionsSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.liga(),
|
||||
kLigaturesType,
|
||||
kCommonLigaturesOnSelector,
|
||||
kCommonLigaturesOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.onum(),
|
||||
kNumberCaseType,
|
||||
kLowerCaseNumbersSelector,
|
||||
2,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ordn(),
|
||||
kVerticalPositionType,
|
||||
kOrdinalsSelector,
|
||||
kNormalPositionSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.pnum(),
|
||||
kNumberSpacingType,
|
||||
kProportionalNumbersSelector,
|
||||
4,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss01(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltOneOnSelector,
|
||||
kStylisticAltOneOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss02(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltTwoOnSelector,
|
||||
kStylisticAltTwoOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss03(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltThreeOnSelector,
|
||||
kStylisticAltThreeOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss04(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltFourOnSelector,
|
||||
kStylisticAltFourOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss05(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltFiveOnSelector,
|
||||
kStylisticAltFiveOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss06(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltSixOnSelector,
|
||||
kStylisticAltSixOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss07(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltSevenOnSelector,
|
||||
kStylisticAltSevenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss08(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltEightOnSelector,
|
||||
kStylisticAltEightOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss09(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltNineOnSelector,
|
||||
kStylisticAltNineOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss10(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltTenOnSelector,
|
||||
kStylisticAltTenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss11(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltElevenOnSelector,
|
||||
kStylisticAltElevenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss12(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltTwelveOnSelector,
|
||||
kStylisticAltTwelveOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss13(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltThirteenOnSelector,
|
||||
kStylisticAltThirteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss14(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltFourteenOnSelector,
|
||||
kStylisticAltFourteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss15(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltFifteenOnSelector,
|
||||
kStylisticAltFifteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss16(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltSixteenOnSelector,
|
||||
kStylisticAltSixteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss17(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltSeventeenOnSelector,
|
||||
kStylisticAltSeventeenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss18(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltEighteenOnSelector,
|
||||
kStylisticAltEighteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss19(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltNineteenOnSelector,
|
||||
kStylisticAltNineteenOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.ss20(),
|
||||
kStylisticAlternativesType,
|
||||
kStylisticAltTwentyOnSelector,
|
||||
kStylisticAltTwentyOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.subs(),
|
||||
kVerticalPositionType,
|
||||
kInferiorsSelector,
|
||||
kNormalPositionSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.sups(),
|
||||
kVerticalPositionType,
|
||||
kSuperiorsSelector,
|
||||
kNormalPositionSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.swsh(),
|
||||
kContextualAlternatesType,
|
||||
kSwashAlternatesOnSelector,
|
||||
kSwashAlternatesOffSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.titl(),
|
||||
kStyleOptionsType,
|
||||
kTitlingCapsSelector,
|
||||
kNoStyleOptionsSelector,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.tnum(),
|
||||
kNumberSpacingType,
|
||||
kMonospacedNumbersSelector,
|
||||
4,
|
||||
);
|
||||
toggle_open_type_feature(
|
||||
font,
|
||||
features.zero(),
|
||||
kTypographicExtrasType,
|
||||
kSlashedZeroOnSelector,
|
||||
kSlashedZeroOffSelector,
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_open_type_feature(
|
||||
font: &mut Font,
|
||||
enabled: Option<bool>,
|
||||
type_identifier: i32,
|
||||
on_selector_identifier: i32,
|
||||
off_selector_identifier: i32,
|
||||
) {
|
||||
if let Some(enabled) = enabled {
|
||||
unsafe {
|
||||
let native_font = font.native_font();
|
||||
unsafe {
|
||||
let selector_identifier = if enabled {
|
||||
on_selector_identifier
|
||||
} else {
|
||||
off_selector_identifier
|
||||
};
|
||||
let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
|
||||
native_font.copy_descriptor().as_concrete_TypeRef(),
|
||||
CFNumber::from(type_identifier).as_concrete_TypeRef(),
|
||||
CFNumber::from(selector_identifier).as_concrete_TypeRef(),
|
||||
let mut feature_array =
|
||||
CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
|
||||
for (tag, value) in features.tag_value_list() {
|
||||
let keys = [kCTFontOpenTypeFeatureTag, kCTFontOpenTypeFeatureValue];
|
||||
let values = [
|
||||
CFString::new(&tag).as_CFTypeRef(),
|
||||
CFNumber::from(*value as i32).as_CFTypeRef(),
|
||||
];
|
||||
let dict = CFDictionaryCreate(
|
||||
kCFAllocatorDefault,
|
||||
&keys as *const _ as _,
|
||||
&values as *const _ as _,
|
||||
2,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
|
||||
let new_font = CTFontCreateCopyWithAttributes(
|
||||
font.native_font().as_concrete_TypeRef(),
|
||||
0.0,
|
||||
ptr::null(),
|
||||
new_descriptor.as_concrete_TypeRef(),
|
||||
);
|
||||
let new_font = CTFont::wrap_under_create_rule(new_font);
|
||||
*font = Font::from_native_font(&new_font);
|
||||
values.into_iter().for_each(|value| CFRelease(value));
|
||||
CFArrayAppendValue(feature_array, dict as _);
|
||||
CFRelease(dict as _);
|
||||
}
|
||||
let attrs = CFDictionaryCreate(
|
||||
kCFAllocatorDefault,
|
||||
&kCTFontFeatureSettingsAttribute as *const _ as _,
|
||||
&feature_array as *const _ as _,
|
||||
1,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
CFRelease(feature_array as *const _ as _);
|
||||
let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
|
||||
CFRelease(attrs as _);
|
||||
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
|
||||
let new_font = CTFontCreateCopyWithAttributes(
|
||||
font.native_font().as_concrete_TypeRef(),
|
||||
0.0,
|
||||
ptr::null(),
|
||||
new_descriptor.as_concrete_TypeRef(),
|
||||
);
|
||||
let new_font = CTFont::wrap_under_create_rule(new_font);
|
||||
*font = Font::from_native_font(&new_font);
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "CoreText", kind = "framework")]
|
||||
extern "C" {
|
||||
static kCTFontOpenTypeFeatureTag: CFStringRef;
|
||||
static kCTFontOpenTypeFeatureValue: CFStringRef;
|
||||
|
||||
fn CTFontCreateCopyWithAttributes(
|
||||
font: CTFontRef,
|
||||
size: CGFloat,
|
||||
|
||||
@@ -20,7 +20,7 @@ use cocoa::{
|
||||
},
|
||||
};
|
||||
use core_foundation::{
|
||||
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
|
||||
base::{CFRelease, CFType, CFTypeRef, OSStatus, TCFType as _},
|
||||
boolean::CFBoolean,
|
||||
data::CFData,
|
||||
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
|
||||
@@ -120,6 +120,10 @@ unsafe fn build_classes() {
|
||||
sel!(menuWillOpen:),
|
||||
menu_will_open as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(applicationDockMenu:),
|
||||
handle_dock_menu as extern "C" fn(&mut Object, Sel, id) -> id,
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(application:openURLs:),
|
||||
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
||||
@@ -147,6 +151,7 @@ pub(crate) struct MacPlatformState {
|
||||
menu_actions: Vec<Box<dyn Action>>,
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
@@ -174,6 +179,7 @@ impl MacPlatform {
|
||||
menu_actions: Default::default(),
|
||||
open_urls: None,
|
||||
finish_launching: None,
|
||||
dock_menu: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -226,6 +232,27 @@ impl MacPlatform {
|
||||
application_menu
|
||||
}
|
||||
|
||||
unsafe fn create_dock_menu(
|
||||
&self,
|
||||
menu_items: Vec<MenuItem>,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
) -> id {
|
||||
let dock_menu = NSMenu::new(nil);
|
||||
dock_menu.setDelegate_(delegate);
|
||||
for item_config in menu_items {
|
||||
dock_menu.addItem_(Self::create_menu_item(
|
||||
item_config,
|
||||
delegate,
|
||||
actions,
|
||||
keymap,
|
||||
));
|
||||
}
|
||||
|
||||
dock_menu
|
||||
}
|
||||
|
||||
unsafe fn create_menu_item(
|
||||
item: MenuItem,
|
||||
delegate: id,
|
||||
@@ -731,6 +758,18 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
let new = self.create_dock_menu(menu, app.delegate(), actions, keymap);
|
||||
if let Some(old) = state.dock_menu.replace(new) {
|
||||
CFRelease(old as _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_recent_document(&self, path: &Path) {
|
||||
if let Some(path_str) = path.to_str() {
|
||||
unsafe {
|
||||
@@ -817,8 +856,6 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, _item: ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
unsafe {
|
||||
@@ -856,10 +893,6 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
unsafe {
|
||||
@@ -1134,6 +1167,18 @@ extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_dock_menu(this: &mut Object, _: Sel, _: id) -> id {
|
||||
unsafe {
|
||||
let platform = get_mac_platform(this);
|
||||
let mut state = platform.0.lock();
|
||||
if let Some(id) = state.dock_menu {
|
||||
id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
NSString::alloc(nil).init_str(string).autorelease()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub(crate) struct TestPlatform {
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
active_cursor: Mutex<CursorStyle>,
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(target_os = "linux")]
|
||||
current_primary_item: Mutex<Option<ClipboardItem>>,
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
@@ -45,6 +46,7 @@ impl TestPlatform {
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
active_window: Default::default(),
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
#[cfg(target_os = "linux")]
|
||||
current_primary_item: Mutex::new(None),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
@@ -231,6 +233,7 @@ impl Platform for TestPlatform {
|
||||
}
|
||||
|
||||
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
|
||||
fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
|
||||
|
||||
fn add_recent_document(&self, _paths: &Path) {}
|
||||
|
||||
@@ -272,6 +275,7 @@ impl Platform for TestPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
@@ -280,6 +284,7 @@ impl Platform for TestPlatform {
|
||||
*self.current_clipboard_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem> {
|
||||
self.current_primary_item.lock().clone()
|
||||
}
|
||||
|
||||
@@ -1201,26 +1201,26 @@ fn apply_font_features(
|
||||
|
||||
// All of these features are enabled by default by DirectWrite.
|
||||
// If you want to (and can) peek into the source of DirectWrite
|
||||
let mut feature_liga = make_direct_write_feature("liga", true);
|
||||
let mut feature_clig = make_direct_write_feature("clig", true);
|
||||
let mut feature_calt = make_direct_write_feature("calt", true);
|
||||
let mut feature_liga = make_direct_write_feature("liga", 1);
|
||||
let mut feature_clig = make_direct_write_feature("clig", 1);
|
||||
let mut feature_calt = make_direct_write_feature("calt", 1);
|
||||
|
||||
for (tag, enable) in tag_values {
|
||||
if tag == *"liga" && !enable {
|
||||
for (tag, value) in tag_values {
|
||||
if tag.as_str() == "liga" && *value == 0 {
|
||||
feature_liga.parameter = 0;
|
||||
continue;
|
||||
}
|
||||
if tag == *"clig" && !enable {
|
||||
if tag.as_str() == "clig" && *value == 0 {
|
||||
feature_clig.parameter = 0;
|
||||
continue;
|
||||
}
|
||||
if tag == *"calt" && !enable {
|
||||
if tag.as_str() == "calt" && *value == 0 {
|
||||
feature_calt.parameter = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
|
||||
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?;
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
@@ -1233,18 +1233,11 @@ fn apply_font_features(
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn make_direct_write_feature(feature_name: &str, enable: bool) -> DWRITE_FONT_FEATURE {
|
||||
fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_FEATURE {
|
||||
let tag = make_direct_write_tag(feature_name);
|
||||
if enable {
|
||||
DWRITE_FONT_FEATURE {
|
||||
nameTag: tag,
|
||||
parameter: 1,
|
||||
}
|
||||
} else {
|
||||
DWRITE_FONT_FEATURE {
|
||||
nameTag: tag,
|
||||
parameter: 0,
|
||||
}
|
||||
DWRITE_FONT_FEATURE {
|
||||
nameTag: tag,
|
||||
parameter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,11 @@ pub(crate) fn handle_msg(
|
||||
WM_XBUTTONUP => handle_xbutton_msg(wparam, lparam, handle_mouse_up_msg, state_ptr),
|
||||
WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr),
|
||||
WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr),
|
||||
WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr),
|
||||
WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, state_ptr),
|
||||
WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr),
|
||||
WM_KEYUP => handle_keyup_msg(handle, wparam, state_ptr),
|
||||
WM_CHAR => handle_char_msg(handle, wparam, lparam, state_ptr),
|
||||
WM_SYSKEYDOWN => handle_syskeydown_msg(wparam, lparam, state_ptr),
|
||||
WM_SYSKEYUP => handle_syskeyup_msg(wparam, state_ptr),
|
||||
WM_KEYDOWN => handle_keydown_msg(wparam, lparam, state_ptr),
|
||||
WM_KEYUP => handle_keyup_msg(wparam, state_ptr),
|
||||
WM_CHAR => handle_char_msg(wparam, lparam, state_ptr),
|
||||
WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
|
||||
WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
|
||||
WM_SETCURSOR => handle_set_cursor(lparam, state_ptr),
|
||||
@@ -179,15 +179,13 @@ fn handle_timer_msg(
|
||||
}
|
||||
|
||||
fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let mut paint_struct = PAINTSTRUCT::default();
|
||||
let _hdc = unsafe { BeginPaint(handle, &mut paint_struct) };
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut request_frame) = lock.callbacks.request_frame.take() {
|
||||
drop(lock);
|
||||
request_frame();
|
||||
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
|
||||
}
|
||||
unsafe { EndPaint(handle, &paint_struct).ok().log_err() };
|
||||
unsafe { ValidateRect(handle, None).ok().log_err() };
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -261,7 +259,6 @@ fn handle_mouse_move_msg(
|
||||
}
|
||||
|
||||
fn handle_syskeydown_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
@@ -281,7 +278,6 @@ fn handle_syskeydown_msg(
|
||||
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||
};
|
||||
let result = if func(PlatformInput::KeyDown(event)).default_prevented {
|
||||
invalidate_client_area(handle);
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
@@ -291,11 +287,7 @@ fn handle_syskeydown_msg(
|
||||
result
|
||||
}
|
||||
|
||||
fn handle_syskeyup_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
fn handle_syskeyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
|
||||
// shortcuts.
|
||||
let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else {
|
||||
@@ -308,7 +300,6 @@ fn handle_syskeyup_msg(
|
||||
drop(lock);
|
||||
let event = KeyUpEvent { keystroke };
|
||||
let result = if func(PlatformInput::KeyUp(event)).default_prevented {
|
||||
invalidate_client_area(handle);
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
@@ -319,7 +310,6 @@ fn handle_syskeyup_msg(
|
||||
}
|
||||
|
||||
fn handle_keydown_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
@@ -337,7 +327,6 @@ fn handle_keydown_msg(
|
||||
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||
};
|
||||
let result = if func(PlatformInput::KeyDown(event)).default_prevented {
|
||||
invalidate_client_area(handle);
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
@@ -347,11 +336,7 @@ fn handle_keydown_msg(
|
||||
result
|
||||
}
|
||||
|
||||
fn handle_keyup_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
fn handle_keyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let Some(keystroke) = parse_keydown_msg_keystroke(wparam) else {
|
||||
return Some(1);
|
||||
};
|
||||
@@ -362,7 +347,6 @@ fn handle_keyup_msg(
|
||||
drop(lock);
|
||||
let event = KeyUpEvent { keystroke };
|
||||
let result = if func(PlatformInput::KeyUp(event)).default_prevented {
|
||||
invalidate_client_area(handle);
|
||||
Some(0)
|
||||
} else {
|
||||
Some(1)
|
||||
@@ -373,7 +357,6 @@ fn handle_keyup_msg(
|
||||
}
|
||||
|
||||
fn handle_char_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
@@ -396,7 +379,6 @@ fn handle_char_msg(
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
lock.callbacks.input = Some(func);
|
||||
if dispatch_event_result.default_prevented || !dispatch_event_result.propagate {
|
||||
invalidate_client_area(handle);
|
||||
return Some(0);
|
||||
}
|
||||
let Some(ime_char) = ime_key else {
|
||||
@@ -407,7 +389,6 @@ fn handle_char_msg(
|
||||
};
|
||||
drop(lock);
|
||||
input_handler.replace_text_in_range(None, &ime_char);
|
||||
invalidate_client_area(handle);
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
|
||||
Some(0)
|
||||
@@ -648,7 +629,6 @@ fn handle_ime_composition(
|
||||
drop(lock);
|
||||
input_handler.replace_text_in_range(None, &comp_result);
|
||||
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
|
||||
invalidate_client_area(handle);
|
||||
return Some(0);
|
||||
}
|
||||
// currently, we don't care other stuff
|
||||
@@ -771,7 +751,6 @@ fn handle_dpi_changed_msg(
|
||||
.context("unable to set window position after dpi has changed")
|
||||
.log_err();
|
||||
}
|
||||
invalidate_client_area(handle);
|
||||
|
||||
Some(0)
|
||||
}
|
||||
@@ -1161,12 +1140,6 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
|
||||
}
|
||||
}
|
||||
|
||||
/// mark window client rect to be re-drawn
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-invalidaterect
|
||||
pub(crate) fn invalidate_client_area(handle: HWND) {
|
||||
unsafe { InvalidateRect(handle, None, FALSE).ok().log_err() };
|
||||
}
|
||||
|
||||
fn parse_ime_compostion_string(handle: HWND) -> Option<(String, usize)> {
|
||||
unsafe {
|
||||
let ctx = ImmGetContext(handle);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user