Compare commits

..

3 Commits

Author SHA1 Message Date
Thorsten Ball
d5ff62cf3a zed 0.136.1 2024-05-16 14:35:22 +02:00
Toon Willems
c44d170cb1 Fix: Missing token count for GPT-4o model. (bumps tiktoken-rs to v0.5.9) (#11893)
Fix: this makes sure we have token counts for the new GPT-4o model.

See: https://github.com/zurawiki/tiktoken-rs/releases/tag/v0.5.9 

Release Notes:

- Fix: Token count was missing for the new GPT-4o model.

(I believe this should go in a 0.136.x release)
2024-05-16 14:24:34 +02:00
Joseph T. Lyons
dbe636ebe2 v0.136.x preview 2024-05-15 11:47:30 -04:00
294 changed files with 4962 additions and 12739 deletions

View File

@@ -1,15 +0,0 @@
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.

View File

@@ -1,14 +0,0 @@
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;
},
};

View File

@@ -1,8 +0,0 @@
name = "docs-proxy"
main = "src/worker.js"
compatibility_date = "2024-05-03"
workers_dev = true
[[routes]]
pattern = "zed.dev/docs*"
zone_name = "zed.dev"

View File

@@ -1,19 +0,0 @@
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,
});
},
};

View File

@@ -1,8 +0,0 @@
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'

View File

@@ -1,56 +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 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 Normal file
View File

@@ -0,0 +1,35 @@
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
View File

@@ -24,4 +24,3 @@ DerivedData/
.venv
.blob_store
.vscode
.wrangler

View File

@@ -3,5 +3,10 @@
"label": "clippy",
"command": "cargo",
"args": ["xtask", "clippy"]
},
{
"label": "assistant2",
"command": "cargo",
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
}
]

302
Cargo.lock generated
View File

@@ -70,7 +70,6 @@ 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",
@@ -348,9 +347,7 @@ dependencies = [
"file_icons",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"gray_matter",
"http 0.1.0",
"indoc",
"language",
@@ -360,24 +357,20 @@ dependencies = [
"open_ai",
"ordered-float 2.10.0",
"parking_lot",
"picker",
"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",
@@ -1688,7 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"regex-automata 0.3.9",
"regex-automata 0.3.8",
"serde",
]
@@ -2098,7 +2091,7 @@ dependencies = [
"bitflags 1.3.2",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"strsim 0.10.0",
"strsim",
"termcolor",
"textwrap",
]
@@ -2122,7 +2115,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex 0.5.1",
"strsim 0.10.0",
"strsim",
]
[[package]]
@@ -2419,6 +2412,7 @@ dependencies = [
"call",
"channel",
"client",
"clock",
"collections",
"db",
"dev_server_projects",
@@ -2567,26 +2561,6 @@ 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"
@@ -2827,7 +2801,7 @@ dependencies = [
"cranelift-entity",
"cranelift-isle",
"gimli",
"hashbrown 0.14.5",
"hashbrown 0.14.0",
"log",
"regalloc2",
"smallvec",
@@ -3147,7 +3121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"hashbrown 0.14.0",
"lock_api",
"once_cell",
"parking_lot_core",
@@ -3639,12 +3613,11 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.4.5"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
dependencies = [
"serde",
"typeid",
]
[[package]]
@@ -3790,7 +3763,6 @@ dependencies = [
"node_runtime",
"parking_lot",
"project",
"release_channel",
"schemars",
"semantic_version",
"serde",
@@ -3845,7 +3817,6 @@ dependencies = [
"language",
"picker",
"project",
"release_channel",
"semantic_version",
"serde",
"settings",
@@ -3979,7 +3950,6 @@ dependencies = [
"menu",
"picker",
"project",
"serde",
"serde_json",
"settings",
"text",
@@ -4527,7 +4497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
dependencies = [
"fallible-iterator",
"indexmap 2.2.6",
"indexmap 2.0.0",
"stable_deref_trait",
]
@@ -4640,6 +4610,7 @@ name = "go_to_line"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"gpui",
"indoc",
@@ -4782,7 +4753,6 @@ dependencies = [
"windows 0.56.0",
"windows-core 0.56.0",
"x11rb",
"xim",
"xkbcommon",
]
@@ -4795,18 +4765,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "gray_matter"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
dependencies = [
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "grid"
version = "0.13.0"
@@ -4873,9 +4831,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.5"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
dependencies = [
"ahash 0.8.8",
"allocator-api2",
@@ -4887,7 +4845,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.5",
"hashbrown 0.14.0",
]
[[package]]
@@ -5305,12 +5263,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.6"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"hashbrown 0.14.0",
"serde",
]
@@ -5540,9 +5498,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.11"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jni"
@@ -5944,12 +5902,6 @@ dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -6053,9 +6005,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
dependencies = [
"serde",
"value-bag",
@@ -6086,8 +6038,8 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?branch=apply-snippet-edit#853c7881d200777e20799026651ca36727144646"
version = "0.94.1"
source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -6415,7 +6367,7 @@ dependencies = [
"bitflags 2.4.2",
"codespan-reporting",
"hexf-parse",
"indexmap 2.2.6",
"indexmap 2.0.0",
"log",
"num-traits",
"rustc-hash",
@@ -6845,8 +6797,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
dependencies = [
"crc32fast",
"hashbrown 0.14.5",
"indexmap 2.2.6",
"hashbrown 0.14.0",
"indexmap 2.0.0",
"memchr",
]
@@ -7060,6 +7012,7 @@ dependencies = [
name = "outline"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"fuzzy",
"gpui",
@@ -7278,7 +7231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap 2.2.6",
"indexmap 2.0.0",
]
[[package]]
@@ -7679,7 +7632,6 @@ dependencies = [
"client",
"clock",
"collections",
"dev_server_projects",
"env_logger",
"fs",
"futures 0.3.28",
@@ -7707,12 +7659,9 @@ dependencies = [
"serde_json",
"settings",
"sha2 0.10.7",
"shlex",
"similar",
"smol",
"snippet",
"task",
"tempfile",
"terminal",
"text",
"unindent",
@@ -8067,14 +8016,12 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"dev_server_projects",
"editor",
"feature_flags",
"fuzzy",
"gpui",
"language",
"markdown",
"menu",
"ordered-float 2.10.0",
"picker",
@@ -8082,9 +8029,9 @@ dependencies = [
"rpc",
"serde",
"serde_json",
"settings",
"smol",
"task",
"terminal_view",
"theme",
"ui",
"ui_text_field",
"util",
@@ -8172,9 +8119,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.9"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
[[package]]
name = "regex-automata"
@@ -8657,9 +8604,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "safemem"
@@ -8988,18 +8935,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.202"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
@@ -9028,11 +8975,11 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.117"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"indexmap 2.2.6",
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
@@ -9044,7 +8991,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e"
dependencies = [
"indexmap 2.2.6",
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
@@ -9560,7 +9507,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap 2.2.6",
"indexmap 2.0.0",
"log",
"memchr",
"once_cell",
@@ -9766,6 +9713,7 @@ name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant2",
"clap 4.4.4",
"collab_ui",
"ctrlc",
@@ -9814,12 +9762,6 @@ 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"
@@ -10153,7 +10095,6 @@ dependencies = [
"gpui",
"language",
"menu",
"parking_lot",
"picker",
"project",
"schemars",
@@ -10161,7 +10102,6 @@ dependencies = [
"serde_json",
"settings",
"task",
"text",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
@@ -10457,15 +10397,6 @@ 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"
@@ -10677,7 +10608,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.2.6",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -10690,7 +10621,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap 2.2.6",
"indexmap 2.0.0",
"toml_datetime",
"winnow 0.5.15",
]
@@ -10701,7 +10632,7 @@ version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
dependencies = [
"indexmap 2.2.6",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -10926,8 +10857,8 @@ dependencies = [
[[package]]
name = "tree-sitter-go"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=b82ab803d887002a0af11f6ce63d72884580bf33#b82ab803d887002a0af11f6ce63d72884580bf33"
version = "0.19.1"
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=aeb2f33b366fd78d5789ff104956ce23508b85db#aeb2f33b366fd78d5789ff104956ce23508b85db"
dependencies = [
"cc",
"tree-sitter",
@@ -11125,12 +11056,6 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
[[package]]
name = "typenum"
version = "1.17.0"
@@ -11367,9 +11292,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.9.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
dependencies = [
"value-bag-serde1",
"value-bag-sval2",
@@ -11377,9 +11302,9 @@ dependencies = [
[[package]]
name = "value-bag-serde1"
version = "1.9.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b"
checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
dependencies = [
"erased-serde",
"serde",
@@ -11388,9 +11313,9 @@ dependencies = [
[[package]]
name = "value-bag-sval2"
version = "1.9.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b"
checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
dependencies = [
"sval",
"sval_buffer",
@@ -11641,7 +11566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
dependencies = [
"anyhow",
"indexmap 2.2.6",
"indexmap 2.0.0",
"serde",
"serde_derive",
"serde_json",
@@ -11657,7 +11582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.4.2",
"indexmap 2.2.6",
"indexmap 2.0.0",
"semver",
]
@@ -11684,7 +11609,7 @@ dependencies = [
"cfg-if",
"encoding_rs",
"gimli",
"indexmap 2.2.6",
"indexmap 2.0.0",
"libc",
"log",
"object",
@@ -11815,7 +11740,7 @@ dependencies = [
"cpp_demangle",
"cranelift-entity",
"gimli",
"indexmap 2.2.6",
"indexmap 2.0.0",
"log",
"object",
"rustc-demangle",
@@ -11866,7 +11791,7 @@ dependencies = [
"cc",
"cfg-if",
"encoding_rs",
"indexmap 2.2.6",
"indexmap 2.0.0",
"libc",
"log",
"mach",
@@ -11971,7 +11896,7 @@ checksum = "96326c9800fb6c099f50d1bd2126d636fc2f96950e1675acf358c0f52516cd38"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.2.6",
"indexmap 2.0.0",
"wit-parser",
]
@@ -12666,7 +12591,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.2.6",
"indexmap 2.0.0",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -12694,7 +12619,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"indexmap 2.2.6",
"indexmap 2.0.0",
"log",
"serde",
"serde_derive",
@@ -12713,7 +12638,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.2.6",
"indexmap 2.0.0",
"log",
"semver",
"serde",
@@ -12874,35 +12799,6 @@ 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.5",
"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"
@@ -12943,15 +12839,6 @@ dependencies = [
"toml 0.8.10",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -13042,12 +12929,13 @@ dependencies = [
[[package]]
name = "zed"
version = "0.138.0"
version = "0.136.1"
dependencies = [
"activity_indicator",
"anyhow",
"assets",
"assistant",
"assistant2",
"audio",
"auto_update",
"backtrace",
@@ -13165,33 +13053,33 @@ dependencies = [
name = "zed_dart"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_deno"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elixir"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elm"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_emmet"
version = "0.0.3"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13212,6 +13100,15 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.6"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.6"
@@ -13223,60 +13120,51 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.7"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_gleam"
version = "0.1.3"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_haskell"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_html"
version = "0.1.1"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.4",
]
[[package]]
name = "zed_lua"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_ocaml"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_php"
version = "0.0.4"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13297,30 +13185,30 @@ dependencies = [
[[package]]
name = "zed_ruby"
version = "0.0.4"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_svelte"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_terraform"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_toml"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -13334,14 +13222,14 @@ dependencies = [
name = "zed_vue"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_zig"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.0.7",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View File

@@ -330,7 +330,6 @@ serde_json_lenient = { version = "0.1", features = [
serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -356,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 = "b82ab803d887002a0af11f6ce63d72884580bf33" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
rustc-demangle = "0.1.23"
@@ -376,7 +375,7 @@ unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
uuid = { version = "1.1.2", features = ["v4", "v5"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [
@@ -403,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",

View File

@@ -57,9 +57,7 @@
"gitkeep": "vcs",
"gitmodules": "vcs",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
"graphqls": "graphql",
"h": "c",
"hpp": "cpp",
"handlebars": "code",

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 298 B

View File

@@ -191,12 +191,6 @@
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
"context": "Markdown",
"bindings": {
"ctrl-c": "markdown::Copy"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -501,12 +495,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {

View File

@@ -208,9 +208,11 @@
}
},
{
"context": "Markdown",
"context": "AssistantChat > Editor", // Used in the assistant2 crate
"bindings": {
"cmd-c": "markdown::Copy"
"enter": ["assistant2::Submit", "Simple"],
"cmd-enter": ["assistant2::Submit", "Codebase"],
"escape": "assistant2::Cancel"
}
},
{
@@ -515,12 +517,6 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -577,6 +573,7 @@
"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",

View File

@@ -493,8 +493,8 @@
"shift-o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"shift-d": "vim::VisualDelete",
"shift-x": "vim::VisualDelete",
"y": "vim::VisualYank",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",

View File

@@ -1,18 +1,5 @@
{
// 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
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
@@ -84,15 +71,6 @@
"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.
@@ -316,7 +294,7 @@
// AI provider.
"provider": {
"name": "openai",
// The default model to use when creating new contexts. This
// The default model to use when starting new conversations. This
// setting can take three values:
//
// 1. "gpt-3.5-turbo"
@@ -333,7 +311,9 @@
// 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.
//
@@ -468,7 +448,9 @@
"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": {
@@ -497,7 +479,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,
@@ -579,8 +561,13 @@
// 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"
}
},
@@ -605,7 +592,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"],
@@ -622,30 +609,19 @@
},
// Different settings for specific languages.
"languages": {
"Astro": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
}
},
"Blade": {
"prettier": {
"allowed": true
}
"C++": {
"format_on_save": "off"
},
"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
@@ -655,120 +631,32 @@
"source.organizeImports": true
}
},
"GraphQL": {
"prettier": {
"allowed": true
}
},
"HEEX": {
"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
}
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off",
"prettier": {
"allowed": true
}
},
"PHP": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"]
}
"format_on_save": "off"
},
"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.
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
// project has no other Prettier installed.
"prettier": {
// // 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.
// Use regular Prettier json configuration:
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
@@ -826,17 +714,5 @@
// - `short`: "2 s, 15 l, 32 c"
// - `long`: "2 selections, 15 lines, 32 characters"
// Default: 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
"line_indicator_format": "long"
}

View File

@@ -21,7 +21,6 @@ editor.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
http.workspace = true
indoc.workspace = true
@@ -34,14 +33,12 @@ 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
@@ -50,8 +47,6 @@ ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
gray_matter = "0.2.7"
[dev-dependencies]
ctor.workspace = true
@@ -60,4 +55,3 @@ env_logger.workspace = true
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
unindent.workspace = true

View File

@@ -0,0 +1,3 @@
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.

View File

@@ -9,22 +9,3 @@ 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,
}

View File

@@ -1,4 +1,3 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@@ -9,7 +8,6 @@ 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};
@@ -34,12 +32,10 @@ impl Default for CurrentProjectContext {
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(),
})
.filter(|message| !message.content.is_empty())
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
}
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
@@ -48,12 +44,12 @@ impl CurrentProjectContext {
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;
return;
}
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
@@ -78,16 +74,12 @@ impl CurrentProjectContext {
if let Some(message) = message_task.await.log_err() {
conversation
.update(&mut cx, |conversation, cx| {
.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> {
@@ -95,54 +87,32 @@ impl CurrentProjectContext {
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}")?;
}
let name = cargo_toml
.package
.as_ref()
.map(|package| package.name.as_str());
if let Some(name) = name {
message.push_str(&format!(" named \"{name}\""));
}
message.push_str(". ");
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
let description = cargo_toml
.package
.as_ref()
.and_then(|package| package.description.as_ref())
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
message.push_str("It describes itself as ");
message.push_str(&format!("\"{description}\""));
message.push_str(". ");
}
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}")?;
}
}
let dependencies = cargo_toml.dependencies.keys().cloned().collect::<Vec<_>>();
if !dependencies.is_empty() {
message.push_str("The following dependencies are installed: ");
message.push_str(&dependencies.join(", "));
message.push_str(". ");
}
Ok(message)

View File

@@ -1,14 +1,12 @@
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 gpui::{Subscription, Task, WeakModel};
use language::Buffer;
use super::ContextUpdated;
use crate::{LanguageModelRequestMessage, Role};
pub struct RecentBuffersContext {
pub enabled: bool,
pub buffers: Vec<RecentBuffer>,
pub snapshot: RecentBuffersSnapshot,
pub message: String,
pub pending_message: Option<Task<()>>,
}
@@ -22,126 +20,18 @@ impl Default for RecentBuffersContext {
Self {
enabled: true,
buffers: Vec::new(),
snapshot: RecentBuffersSnapshot::default(),
message: String::new(),
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(),
})
.filter(|message| !message.content.is_empty())
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
}
}
#[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())
}

View File

@@ -3,21 +3,16 @@ pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod omit_ranges;
mod prompts;
mod saved_conversation;
mod search;
mod slash_command;
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, Global, SharedString, UpdateGlobal};
pub(crate) use prompts::prompt_library::*;
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -36,10 +31,8 @@ actions!(
ToggleFocus,
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
ApplyEdit
]
);
@@ -189,9 +182,6 @@ pub struct LanguageModelChoiceDelta {
struct MessageMetadata {
role: Role,
status: MessageStatus,
// todo!("delete this")
#[serde(skip)]
ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -243,13 +233,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
});
Assistant::update_global(cx, |assistant, cx| {
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);

File diff suppressed because it is too large Load Diff

View File

@@ -339,11 +339,11 @@ pub struct LegacyAssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new contexts.
/// The default OpenAI model to use when starting new conversations.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new contexts.
/// OpenAI API base URL to use when starting new conversations.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
@@ -418,7 +418,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
#[cfg(test)]
mod tests {
use gpui::{AppContext, UpdateGlobal};
use gpui::{AppContext, BorrowAppContext};
use settings::SettingsStore;
use super::*;
@@ -440,7 +440,7 @@ mod tests {
);
// Ensure backward-compatibility.
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{
@@ -460,7 +460,7 @@ mod tests {
low_speed_timeout_in_seconds: None,
}
);
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|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.
SettingsStore::update_global(cx, |store, cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_user_settings(
r#"{

View File

@@ -233,7 +233,7 @@ impl CompletionProvider {
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
CompletionProvider::Fake(_) => unimplemented!(),
}
}

View File

@@ -1,101 +0,0 @@
use rope::Rope;
use std::{cmp::Ordering, ops::Range};
pub(crate) fn text_in_range_omitting_ranges(
rope: &Rope,
range: Range<usize>,
omit_ranges: &[Range<usize>],
) -> String {
let mut content = String::with_capacity(range.len());
let mut omit_ranges = omit_ranges
.iter()
.skip_while(|omit_range| omit_range.end <= range.start)
.peekable();
let mut offset = range.start;
let mut chunks = rope.chunks_in_range(range.clone());
while let Some(chunk) = chunks.next() {
if let Some(omit_range) = omit_ranges.peek() {
match offset.cmp(&omit_range.start) {
Ordering::Less => {
let max_len = omit_range.start - offset;
if chunk.len() < max_len {
content.push_str(chunk);
offset += chunk.len();
} else {
content.push_str(&chunk[..max_len]);
chunks.seek(omit_range.end.min(range.end));
offset = omit_range.end;
omit_ranges.next();
}
}
Ordering::Equal | Ordering::Greater => {
chunks.seek(omit_range.end.min(range.end));
offset = omit_range.end;
omit_ranges.next();
}
}
} else {
content.push_str(chunk);
offset += chunk.len();
}
}
content
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{rngs::StdRng, Rng as _};
use util::RandomCharIter;
#[gpui::test(iterations = 100)]
fn test_text_in_range_omitting_ranges(mut rng: StdRng) {
let text = RandomCharIter::new(&mut rng).take(1024).collect::<String>();
let rope = Rope::from(text.as_str());
let mut start = rng.gen_range(0..=text.len() / 2);
let mut end = rng.gen_range(text.len() / 2..=text.len());
while !text.is_char_boundary(start) {
start -= 1;
}
while !text.is_char_boundary(end) {
end += 1;
}
let range = start..end;
let mut ix = 0;
let mut omit_ranges = Vec::new();
for _ in 0..rng.gen_range(0..10) {
let mut start = rng.gen_range(ix..=text.len());
while !text.is_char_boundary(start) {
start += 1;
}
let mut end = rng.gen_range(start..=text.len());
while !text.is_char_boundary(end) {
end += 1;
}
omit_ranges.push(start..end);
ix = end;
if ix == text.len() {
break;
}
}
let mut expected_text = text[range.clone()].to_string();
for omit_range in omit_ranges.iter().rev() {
let start = omit_range
.start
.saturating_sub(range.start)
.min(range.len());
let end = omit_range.end.saturating_sub(range.start).min(range.len());
expected_text.replace_range(start..end, "");
}
assert_eq!(
text_in_range_omitting_ranges(&rope, range.clone(), &omit_ranges),
expected_text,
"text: {text:?}\nrange: {range:?}\nomit_ranges: {omit_ranges:?}"
);
}
}

View File

@@ -1,3 +1,95 @@
pub mod prompt;
pub mod prompt_library;
pub mod prompt_manager;
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View File

@@ -1,278 +0,0 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
use ui::SharedString;
use gray_matter::{engine::YAML, Matter};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPromptFrontmatter {
title: String,
version: String,
author: String,
#[serde(default)]
languages: Vec<String>,
#[serde(default)]
dependencies: Vec<String>,
}
impl Default for StaticPromptFrontmatter {
fn default() -> Self {
Self {
title: "New Prompt".to_string(),
version: "1.0".to_string(),
author: "No Author".to_string(),
languages: vec!["*".to_string()],
dependencies: vec![],
}
}
}
impl StaticPromptFrontmatter {
pub fn title(&self) -> SharedString {
self.title.clone().into()
}
// pub fn version(&self) -> SharedString {
// self.version.clone().into()
// }
// pub fn author(&self) -> SharedString {
// self.author.clone().into()
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.languages
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.dependencies
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
}
/// A statuc prompt that can be loaded into the prompt library
/// from Markdown with a frontmatter header
///
/// Examples:
///
/// ### Globally available prompt
///
/// ```markdown
/// ---
/// title: Foo
/// version: 1.0
/// author: Jane Kim <jane@kim.com
/// languages: ["*"]
/// dependencies: []
/// ---
///
/// Foo and bar are terms used in programming to describe generic concepts.
/// ```
///
/// ### Language-specific prompt
///
/// ```markdown
/// ---
/// title: UI with GPUI
/// version: 1.0
/// author: Nate Butler <iamnbutler@gmail.com>
/// languages: ["rust"]
/// dependencies: ["gpui"]
/// ---
///
/// When building a UI with GPUI, ensure you...
/// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt {
content: String,
file_name: Option<String>,
}
impl StaticPrompt {
pub fn new(content: String) -> Self {
StaticPrompt {
content,
file_name: None,
}
}
pub fn title(&self) -> Option<SharedString> {
self.metadata().map(|m| m.title())
}
// pub fn version(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.version())
// }
// pub fn author(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.author())
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.metadata().map(|m| m.languages()).unwrap_or_default()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.metadata()
// .map(|m| m.dependencies())
// .unwrap_or_default()
// }
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
// todo!()
// }
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
}
impl StaticPrompt {
// pub fn update(&mut self, contents: String) -> &mut Self {
// self.content = contents;
// self
// }
/// Sets the file name of the prompt
pub fn file_name(&mut self, file_name: String) -> &mut Self {
self.file_name = Some(file_name);
self
}
/// Sets the file name of the prompt based on the title
// pub fn file_name_from_title(&mut self) -> &mut Self {
// if let Some(title) = self.title() {
// let file_name = title.to_lowercase().replace(" ", "_");
// if !file_name.is_empty() {
// self.file_name = Some(file_name);
// }
// }
// self
// }
/// Returns the prompt's content
pub fn content(&self) -> &String {
&self.content
}
fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> {
let matter = Matter::<YAML>::new();
let result = matter.parse(self.content.as_str());
match result.data {
Some(data) => {
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
let body = result.content;
Ok((front_matter, body))
}
None => Err(anyhow::anyhow!("Failed to parse frontmatter")),
}
}
pub fn metadata(&self) -> Option<StaticPromptFrontmatter> {
self.parse().ok().map(|(front_matter, _)| front_matter)
}
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View File

@@ -1,152 +0,0 @@
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::sync::Arc;
use util::paths::PROMPTS_DIR;
use uuid::Uuid;
use super::prompt::StaticPrompt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PromptId(pub Uuid);
#[allow(unused)]
impl PromptId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct PromptLibraryState {
/// A set of prompts that all assistant contexts will start with
default_prompt: Vec<PromptId>,
/// All [Prompt]s loaded into the library
prompts: HashMap<PromptId, StaticPrompt>,
/// Prompts that have been changed but haven't been
/// saved back to the file system
dirty_prompts: Vec<PromptId>,
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::default()),
}
}
pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> {
let state = self.state.read();
state
.prompts
.iter()
.map(|(id, prompt)| (*id, prompt.clone()))
.collect()
}
pub fn first_prompt_id(&self) -> Option<PromptId> {
let state = self.state.read();
state.prompts.keys().next().cloned()
}
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
let state = self.state.read();
state.prompts.get(&id).cloned()
}
/// Save the current state of the prompt library to the
/// file system as a JSON file
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
fs.create_dir(&PROMPTS_DIR).await?;
let path = PROMPTS_DIR.join("index.json");
let json = {
let state = self.state.read();
serde_json::to_string(&*state)?
};
fs.atomic_write(path, json).await?;
Ok(())
}
/// Load the state of the prompt library from the file system
/// or create a new one if it doesn't exist
pub async fn load(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let path = PROMPTS_DIR.join("index.json");
let state = if fs.is_file(&path).await {
let json = fs.load(&path).await?;
serde_json::from_str(&json)?
} else {
PromptLibraryState::default()
};
let mut prompt_library = Self {
state: RwLock::new(state),
};
prompt_library.load_prompts(fs).await?;
Ok(prompt_library)
}
/// Load all prompts from the file system
/// adding them to the library if they don't already exist
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
// let current_prompts = self.all_prompt_contents().clone();
// For now, we'll just clear the prompts and reload them all
self.state.get_mut().prompts.clear();
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
while let Some(prompt_path) = prompt_paths.next().await {
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
if !fs.is_file(&prompt_path).await
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
{
continue;
}
let json = fs
.load(&prompt_path)
.await
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
let mut static_prompt = StaticPrompt::new(json);
if let Some(file_name) = prompt_path.file_name() {
let file_name = file_name.to_string_lossy().into_owned();
static_prompt.file_name(file_name);
}
let state = self.state.get_mut();
let id = Uuid::new_v4();
state.prompts.insert(PromptId(id), static_prompt);
state.version += 1;
}
// Write any changes back to the file system
self.save(fs.clone()).await?;
Ok(())
}
}

View File

@@ -1,327 +0,0 @@
use collections::HashMap;
use editor::Editor;
use fs::Fs;
use gpui::{prelude::FluentBuilder, *};
use language::{language_settings, Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing};
use util::{ResultExt, TryFutureExt};
use workspace::ModalView;
use super::prompt_library::{PromptId, PromptLibrary};
use crate::prompts::prompt::StaticPrompt;
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
#[allow(dead_code)]
fs: Arc<dyn Fs>,
picker: View<Picker<PromptManagerDelegate>>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
}
impl PromptManager {
pub fn new(
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_manager = cx.view().downgrade();
let picker = cx.new_view(|cx| {
Picker::uniform_list(
PromptManagerDelegate {
prompt_manager,
matching_prompts: vec![],
matching_prompt_ids: vec![],
prompt_library: prompt_library.clone(),
selected_index: 0,
},
cx,
)
.max_height(rems(35.75))
.modal(false)
});
let focus_handle = picker.focus_handle(cx);
let mut manager = Self {
focus_handle,
prompt_library,
language_registry,
fs,
picker,
prompt_editors: HashMap::default(),
active_prompt_id: None,
};
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
manager
}
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
self.active_prompt_id = prompt_id;
cx.notify();
}
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
let focus_handle = editor.focus_handle(cx);
cx.focus(&focus_handle)
}
}
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let picker = self.picker.clone();
v_flex()
.id("prompt-list")
.bg(cx.theme().colors().surface_background)
.h_full()
.w_2_5()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h(rems(1.75))
.w_full()
.flex_none()
.justify_between()
.child(Label::new("Prompt Library").size(LabelSize::Small))
.child(IconButton::new("new-prompt", IconName::Plus).disabled(true)),
)
.child(
v_flex()
.h(rems(38.25))
.flex_grow()
.justify_start()
.child(picker),
)
}
fn set_editor_for_prompt(
&mut self,
prompt_id: PromptId,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
cx.new_view(|cx| {
let text = if let Some(prompt) = prompt_library.prompt(prompt_id) {
prompt.content().to_owned()
} else {
"".to_string()
};
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(text, cx);
let markdown = self.language_registry.language_for_name("Markdown");
cx.spawn(|buffer, mut cx| async move {
if let Some(markdown) = markdown.await.log_err() {
_ = buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
});
}
})
.detach();
buffer.set_language_registry(self.language_registry.clone());
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor
})
});
editor_for_prompt.clone()
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.key_context("PromptManager")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
// .on_action(cx.listener(Self::save_active_prompt))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(64.))
.h(rems(40.))
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(
div().w_3_5().h_full().child(
v_flex()
.id("prompt-editor")
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.size_full()
.flex_none()
.min_w_64()
.h_full()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h_7()
.w_full()
.justify_between()
.child(div())
.child(
IconButton::new("dismiss", IconName::Close)
.shape(IconButtonShape::Square)
.on_click(|_, cx| {
cx.dispatch_action(menu::Cancel.boxed_clone());
}),
),
)
.when_some(self.active_prompt_id, |this, active_prompt_id| {
this.child(
h_flex()
.flex_1()
.w_full()
.py(Spacing::Large.rems(cx))
.px(Spacing::XLarge.rems(cx))
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
)
}),
),
)
}
}
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()
}
}
pub struct PromptManagerDelegate {
prompt_manager: WeakView<PromptManager>,
matching_prompts: Vec<Arc<StaticPrompt>>,
matching_prompt_ids: Vec<PromptId>,
prompt_library: Arc<PromptLibrary>,
selected_index: usize,
}
impl PickerDelegate for PromptManagerDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Find a prompt…".into()
}
fn match_count(&self) -> usize {
self.matching_prompt_ids.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn selected_index_changed(
&self,
ix: usize,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
let prompt_manager = self.prompt_manager.upgrade()?;
Some(Box::new(move |cx| {
prompt_manager.update(cx, |manager, cx| {
manager.set_active_prompt(Some(prompt_id), cx);
})
}))
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let prompt_library = self.prompt_library.clone();
cx.spawn(|picker, mut cx| async move {
async {
let prompts = prompt_library.prompts();
let matching_prompts = prompts
.into_iter()
.filter(|(_, prompt)| {
prompt
.content()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect::<Vec<_>>();
picker.update(&mut cx, |picker, cx| {
picker.delegate.matching_prompt_ids =
matching_prompts.iter().map(|(id, _)| *id).collect();
picker.delegate.matching_prompts = matching_prompts
.into_iter()
.map(|(_, prompt)| Arc::new(prompt))
.collect();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
let prompt_manager = self.prompt_manager.upgrade().unwrap();
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
}
fn should_dismiss(&self) -> bool {
false
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.prompt_manager
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let matching_prompt = self.matching_prompts.get(ix)?;
let prompt = matching_prompt.clone();
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(prompt.title().unwrap_or_default().clone())),
)
}
}

View File

@@ -1,171 +0,0 @@
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);
}
}

View File

@@ -1,319 +0,0 @@
use anyhow::Result;
use collections::HashMap;
use editor::{CompletionProvider, Editor};
use futures::channel::oneshot;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WindowHandle};
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::Project;
use rope::Point;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use workspace::Workspace;
use crate::PromptLibrary;
mod current_file_command;
mod file_command;
mod prompt_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
}
#[derive(Default)]
pub(crate) struct SlashCommandRegistry {
commands: HashMap<String, Box<dyn SlashCommand>>,
}
pub(crate) trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn description(&self) -> String;
fn complete_argument(
&self,
query: String,
cancel: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>>;
fn requires_argument(&self) -> bool;
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation;
}
pub(crate) struct SlashCommandInvocation {
pub output: Task<Result<String>>,
pub invalidated: oneshot::Receiver<()>,
pub cleanup: SlashCommandCleanup,
}
#[derive(Default)]
pub(crate) struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
impl SlashCommandCleanup {
pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
Self(Some(Box::new(cleanup)))
}
}
impl Drop for SlashCommandCleanup {
fn drop(&mut self) {
if let Some(cleanup) = self.0.take() {
cleanup();
}
}
}
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
}
impl SlashCommandRegistry {
pub fn new(
project: Model<Project>,
prompt_library: Arc<PromptLibrary>,
window: Option<WindowHandle<Workspace>>,
) -> Arc<Self> {
let mut this = Self {
commands: HashMap::default(),
};
this.register_command(file_command::FileSlashCommand::new(project));
this.register_command(prompt_command::PromptSlashCommand::new(prompt_library));
if let Some(window) = window {
this.register_command(current_file_command::CurrentFileSlashCommand::new(window));
}
Arc::new(this)
}
fn register_command(&mut self, command: impl SlashCommand) {
self.commands.insert(command.name(), Box::new(command));
}
fn command_names(&self) -> impl Iterator<Item = &String> {
self.commands.keys()
}
pub(crate) fn command(&self, name: &str) -> Option<&dyn SlashCommand> {
self.commands.get(name).map(|b| &**b)
}
}
impl SlashCommandCompletionProvider {
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
commands,
}
}
fn complete_command_name(
&self,
command_name: &str,
range: Range<Anchor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
.command_names()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.clone(),
char_bag: def.as_str().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let executor = cx.background_executor().clone();
executor.clone().spawn(async move {
let matches = match_strings(
&candidates,
&command_name,
true,
usize::MAX,
&Default::default(),
executor,
)
.await;
Ok(matches
.into_iter()
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
if command.requires_argument() {
new_text.push(' ');
}
Some(project::Completion {
old_range: range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
new_text,
label: CodeLabel::plain(mat.string, None),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
})
})
.collect())
})
}
fn complete_command_argument(
&self,
command_name: &str,
argument: String,
range: Range<Anchor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
cx.background_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|arg| project::Completion {
old_range: range.clone(),
label: CodeLabel::plain(arg.clone(), None),
new_text: arg.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
})
.collect())
})
} else {
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
}
}
}
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let task = buffer.update(cx, |buffer, cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
let call = SlashCommandLine::parse(line)?;
let name = &line[call.name.clone()];
if let Some(argument) = call.argument {
let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
let argument = line[argument.clone()].to_string();
Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
} else {
let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
Some(self.complete_command_name(name, start..buffer_position, cx))
}
});
task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
}
fn resolve_completions(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
SlashCommandLine::parse(line).is_some()
} else {
false
}
}
}
impl SlashCommandLine {
pub(crate) fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.argument = Some(next_ix..next_ix);
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
argument: None,
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}

View File

@@ -1,135 +0,0 @@
use std::{borrow::Cow, cell::Cell, rc::Rc};
use anyhow::{anyhow, Result};
use collections::HashMap;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
use workspace::{Event as WorkspaceEvent, Workspace};
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
pub(crate) struct CurrentFileSlashCommand {
workspace: WindowHandle<Workspace>,
}
impl CurrentFileSlashCommand {
pub fn new(workspace: WindowHandle<Workspace>) -> Self {
Self { workspace }
}
}
impl SlashCommand for CurrentFileSlashCommand {
fn name(&self) -> String {
"current_file".into()
}
fn description(&self) -> String {
"insert the current file".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(&self, _argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let (invalidate_tx, invalidate_rx) = oneshot::channel();
let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
let mut subscriptions: Vec<Subscription> = Vec::new();
let output = self.workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
let mut most_recent_buffer = None;
for editor in workspace.items_of_type::<Editor>(cx) {
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
continue;
};
let timestamp = timestamps_by_entity_id
.get(&editor.entity_id())
.copied()
.unwrap_or_default();
if most_recent_buffer
.as_ref()
.map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
{
most_recent_buffer = Some((buffer, timestamp));
}
}
subscriptions.push({
let workspace_view = cx.view().clone();
let invalidate_tx = invalidate_tx.clone();
cx.window_context()
.subscribe(&workspace_view, move |_workspace, event, _cx| match event {
WorkspaceEvent::ActiveItemChanged
| WorkspaceEvent::ItemAdded
| WorkspaceEvent::ItemRemoved
| WorkspaceEvent::PaneAdded(_)
| WorkspaceEvent::PaneRemoved => {
if let Some(invalidate_tx) = invalidate_tx.take() {
_ = invalidate_tx.send(());
}
}
_ => {}
})
});
if let Some((buffer, _)) = most_recent_buffer {
subscriptions.push({
let invalidate_tx = invalidate_tx.clone();
cx.window_context().observe(&buffer, move |_buffer, _cx| {
if let Some(invalidate_tx) = invalidate_tx.take() {
_ = invalidate_tx.send(());
}
})
});
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
cx.background_executor().spawn(async move {
let path = path
.as_ref()
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("untitled"));
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
output.push_str("```");
output.push_str(&path);
output.push('\n');
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
Ok(output)
})
} else {
Task::ready(Err(anyhow!("no recent buffer found")))
}
});
SlashCommandInvocation {
output: output.unwrap_or_else(|error| Task::ready(Err(error))),
invalidated: invalidate_rx,
cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
}
}
}

View File

@@ -1,145 +0,0 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use anyhow::Result;
use futures::channel::oneshot;
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task};
use project::{PathMatchCandidateSet, Project};
use std::{
path::Path,
sync::{atomic::AtomicBool, Arc},
};
pub(crate) struct FileSlashCommand {
project: Model<Project>,
}
impl FileSlashCommand {
pub fn new(project: Model<Project>) -> Self {
Self { project }
}
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
let worktrees = self
.project
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
directories_only: false,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
impl SlashCommand for FileSlashCommand {
fn name(&self) -> String {
"file".into()
}
fn description(&self) -> String {
"insert an entire file".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> gpui::Task<Result<Vec<String>>> {
let paths = self.search_paths(query, cancellation_flag, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect())
})
}
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let project = self.project.read(cx);
let Some(argument) = argument else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let path = Path::new(argument);
let abs_path = project.worktrees().find_map(|worktree| {
let worktree = worktree.read(cx);
worktree.entry_for_path(path)?;
worktree.absolutize(path).ok()
});
let Some(abs_path) = abs_path else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let fs = project.fs().clone();
let argument = argument.to_string();
let output = cx.background_executor().spawn(async move {
let content = fs.load(&abs_path).await?;
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
output.push_str(&argument);
output.push('\n');
output.push_str(&content);
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
Ok(output)
});
SlashCommandInvocation {
output,
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
}
}
}

View File

@@ -1,95 +0,0 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use crate::prompts::prompt_library::PromptLibrary;
use anyhow::{anyhow, Context, Result};
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Task};
use std::sync::{atomic::AtomicBool, Arc};
pub(crate) struct PromptSlashCommand {
library: Arc<PromptLibrary>,
}
impl PromptSlashCommand {
pub fn new(library: Arc<PromptLibrary>) -> Self {
Self { library }
}
}
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
"prompt".into()
}
fn description(&self) -> String {
"insert a prompt from the library".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let library = self.library.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
let candidates = library
.prompts()
.into_iter()
.enumerate()
.filter_map(|(ix, prompt)| {
prompt
.1
.title()
.map(|title| StringMatchCandidate::new(ix, title.into()))
})
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&cancellation_flag,
executor,
)
.await;
Ok(matches
.into_iter()
.map(|mat| candidates[mat.candidate_id].string.clone())
.collect())
})
}
fn run(&self, title: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let Some(title) = title else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow!("missing prompt name"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let library = self.library.clone();
let title = title.to_string();
let output = cx.background_executor().spawn(async move {
let prompt = library
.prompts()
.into_iter()
.filter_map(|prompt| prompt.1.title().map(|title| (title, prompt)))
.find(|(t, _)| t == &title)
.with_context(|| format!("no prompt found with title {:?}", title))?
.1;
Ok(prompt.1.content().to_owned())
});
SlashCommandInvocation {
output,
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
}
}
}

View File

@@ -1,86 +0,0 @@
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
}
---
```

View File

@@ -24,8 +24,7 @@ use fs::Fs;
use futures::{future::join_all, StreamExt};
use gpui::{
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
FocusableView, ListAlignment, ListState, Model, ReadGlobal, Render, Task, UpdateGlobal, View,
WeakView,
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
@@ -125,7 +124,7 @@ impl AssistantPanel {
})?;
cx.new_view(|cx| {
let project_index = SemanticIndex::update_global(cx, |semantic_index, cx| {
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
semantic_index.project_index(project.clone(), cx)
});
@@ -289,7 +288,7 @@ impl AssistantChat {
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::global(cx).default_model();
let model = CompletionProvider::get(cx).default_model();
let view = cx.view().downgrade();
let list_state = ListState::new(
0,
@@ -440,7 +439,7 @@ impl AssistantChat {
Markdown::new(
text,
self.markdown_style.clone(),
Some(self.language_registry.clone()),
self.language_registry.clone(),
cx,
)
});
@@ -551,7 +550,7 @@ impl AssistantChat {
let messages = messages.await?;
let completion = cx.update(|cx| {
CompletionProvider::global(cx).complete(
CompletionProvider::get(cx).complete(
model_name,
messages,
Vec::new(),
@@ -573,7 +572,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
this.markdown_style.clone(),
Some(this.language_registry.clone()),
this.language_registry.clone(),
cx,
)
}),
@@ -667,7 +666,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
self.markdown_style.clone(),
Some(self.language_registry.clone()),
self.language_registry.clone(),
cx,
)
}),
@@ -683,7 +682,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
self.markdown_style.clone(),
Some(self.language_registry.clone()),
self.language_registry.clone(),
cx,
)
}),
@@ -1108,7 +1107,7 @@ impl Render for AssistantChat {
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Context", cx)),
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
)
.child(
IconButton::new("assistant-menu", IconName::Menu)

View File

@@ -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::Global;
use gpui::{AppContext, Global};
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
@@ -11,6 +11,10 @@ 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))
}

View File

@@ -3,7 +3,7 @@ use crate::{
AssistantChat, CompletionProvider,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AnyElement, FontStyle, FontWeight, ReadGlobal, TextStyle, View, WeakView, WhiteSpace};
use gpui::{AnyElement, FontStyle, FontWeight, 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::global(cx).available_models() {
for model in CompletionProvider::get(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();

View File

@@ -123,7 +123,6 @@ impl Channel {
}
}
#[derive(Debug)]
pub struct ChannelMembership {
pub user: Arc<User>,
pub kind: proto::channel_member::Kind,
@@ -816,11 +815,9 @@ impl ChannelStore {
Ok(())
})
}
pub fn fuzzy_search_members(
pub fn get_channel_member_details(
&self,
channel_id: ChannelId,
query: String,
limit: u16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
@@ -829,24 +826,26 @@ impl ChannelStore {
let response = client
.request(proto::GetChannelMembers {
channel_id: channel_id.0,
query,
limit: limit as u64,
})
.await?;
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()
})
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())
})
}

View File

@@ -17,7 +17,8 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, BorrowAppContext, Global, Model,
Task, WeakModel,
};
use http::{HttpClient, HttpClientWithUrl};
use lazy_static::lazy_static;
@@ -28,7 +29,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};
use settings::{Settings, SettingsSources, SettingsStore};
use std::fmt;
use std::pin::Pin;
use std::{
@@ -85,7 +86,7 @@ lazy_static! {
}
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut, Reconnect]);
@@ -113,35 +114,11 @@ 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);
ClientSettings::register(cx);
ProxySettings::register(cx);
cx.update_global(|store: &mut SettingsStore, cx| {
store.register_setting::<ClientSettings>(cx);
});
}
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
@@ -535,7 +512,6 @@ 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)
}

View File

@@ -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, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
MemoryEvent, SettingEvent,
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent,
SettingEvent,
};
use tempfile::NamedTempFile;
#[cfg(not(debug_assertions))]
@@ -241,14 +241,14 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_event(
pub fn report_copilot_event(
self: &Arc<Self>,
provider: String,
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
) {
let event = Event::InlineCompletion(InlineCompletionEvent {
provider,
let event = Event::Copilot(CopilotEvent {
suggestion_id,
suggestion_accepted,
file_extension,
});

View File

@@ -89,7 +89,6 @@ 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>>>,
@@ -145,7 +144,6 @@ impl UserStore {
];
Self {
users: Default::default(),
by_github_login: Default::default(),
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
@@ -233,7 +231,6 @@ 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(
@@ -647,12 +644,6 @@ 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()
}
@@ -670,31 +661,26 @@ 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;
let users = response
.users
.into_iter()
.map(User::new)
.collect::<Vec<_>>();
this.update(&mut cx, |this, _| this.insert(users))
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
})
.ok();
Ok(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>,

View File

@@ -407,7 +407,6 @@ 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
);

View File

@@ -1 +0,0 @@
ALTER TABLE dev_servers ADD COLUMN ssh_connection_string TEXT;

View File

@@ -15,9 +15,8 @@ use serde::{Serialize, Serializer};
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent,
SettingEvent,
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
};
use uuid::Uuid;
@@ -27,7 +26,6 @@ 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))
}
@@ -282,77 +280,30 @@ 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("Panic request".to_string())))
w.add_section(|s| s.text(slack::Text::markdown("Possible Hang".to_string())))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!(
"*Version:*\n {} ",
panic.app_version
report.app_version.unwrap_or_default()
)))
.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!(
"*OS:*\n{} {}",
panic.os_name,
panic.os_version.unwrap_or_default()
"*Incident:*\n<https://{}.{}/{}.hang.json|{}…>",
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
@@ -425,19 +376,13 @@ pub async fn post_events(
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::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::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,
@@ -519,7 +464,7 @@ pub async fn post_events(
#[derive(Default)]
struct ToUpload {
editor_events: Vec<EditorEventRow>,
inline_completion_events: Vec<InlineCompletionEventRow>,
copilot_events: Vec<CopilotEventRow>,
assistant_events: Vec<AssistantEventRow>,
call_events: Vec<CallEventRow>,
cpu_events: Vec<CpuEventRow>,
@@ -538,14 +483,14 @@ impl ToUpload {
.await
.with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
const COPILOT_EVENTS_TABLE: &str = "copilot_events";
Self::upload_to_table(
INLINE_COMPLETION_EVENTS_TABLE,
&self.inline_completion_events,
COPILOT_EVENTS_TABLE,
&self.copilot_events,
clickhouse_client,
)
.await
.with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
.with_context(|| format!("failed to upload to table '{COPILOT_EVENTS_TABLE}'"))?;
const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
Self::upload_to_table(
@@ -645,7 +590,7 @@ where
let country_code = country_code.as_bytes();
serializer.serialize_u16(((country_code[1] as u16) << 8) + country_code[0] as u16)
serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16)
}
#[derive(Serialize, Debug, clickhouse::Row)]
@@ -715,9 +660,9 @@ impl EditorEventRow {
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct InlineCompletionEventRow {
pub struct CopilotEventRow {
pub installation_id: String,
pub provider: String,
pub suggestion_id: String,
pub suggestion_accepted: bool,
pub app_version: String,
pub file_extension: String,
@@ -737,9 +682,9 @@ pub struct InlineCompletionEventRow {
pub patch: Option<i32>,
}
impl InlineCompletionEventRow {
impl CopilotEventRow {
fn from_event(
event: InlineCompletionEvent,
event: CopilotEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
@@ -766,7 +711,7 @@ impl InlineCompletionEventRow {
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
provider: event.provider,
suggestion_id: event.suggestion_id.unwrap_or_default(),
suggestion_accepted: event.suggestion_accepted,
}
}

View File

@@ -509,7 +509,8 @@ pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
pub struct CreatedChannelMessage {
pub message_id: MessageId,
pub participant_connection_ids: HashSet<ConnectionId>,
pub participant_connection_ids: Vec<ConnectionId>,
pub channel_members: Vec<UserId>,
pub notifications: NotificationBatch,
}

View File

@@ -440,7 +440,12 @@ impl Database {
channel_id: ChannelId,
user: UserId,
operations: &[proto::Operation],
) -> Result<(HashSet<ConnectionId>, i32, Vec<proto::VectorClockEntry>)> {
) -> Result<(
Vec<ConnectionId>,
Vec<UserId>,
i32,
Vec<proto::VectorClockEntry>,
)> {
self.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
@@ -474,6 +479,7 @@ 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() {
@@ -498,6 +504,12 @@ 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([
@@ -512,10 +524,11 @@ impl Database {
.exec(&*tx)
.await?;
} else {
channel_members = Vec::new();
max_version = Vec::new();
}
let mut connections = HashSet::default();
let mut connections = Vec::new();
let mut rows = channel_buffer_collaborator::Entity::find()
.filter(
Condition::all()
@@ -525,13 +538,13 @@ impl Database {
.await?;
while let Some(row) = rows.next().await {
let row = row?;
connections.insert(ConnectionId {
connections.push(ConnectionId {
id: row.connection_id as u32,
owner_id: row.connection_server_id.0 as u32,
});
}
Ok((connections, buffer.epoch, max_version))
Ok((connections, channel_members, buffer.epoch, max_version))
})
.await
}

View File

@@ -3,7 +3,7 @@ use rpc::{
proto::{channel_member::Kind, ChannelBufferVersion, VectorClockEntry},
ErrorCode, ErrorCodeExt,
};
use sea_orm::{DbBackend, TryGetableMany};
use sea_orm::TryGetableMany;
impl Database {
#[cfg(test)]
@@ -700,73 +700,77 @@ impl Database {
pub async fn get_channel_participant_details(
&self,
channel_id: ChannelId,
filter: &str,
limit: u64,
user_id: UserId,
) -> Result<(Vec<proto::ChannelMember>, Vec<proto::User>)> {
let members = self
) -> Result<Vec<proto::ChannelMember>> {
let (role, members) = self
.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_participant(&channel, user_id, &tx)
let role = self
.check_user_is_channel_participant(&channel, user_id, &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)
Ok((
role,
self.get_channel_participant_details_internal(&channel, &tx)
.await?,
))
})
.await?;
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 {
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 {
Kind::Member
} else {
Kind::Invitee
}
.into(),
}
})
.collect();
})
.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());
}
}
Ok((members, users))
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())
}
/// Returns whether the given user is an admin in the specified channel.

View File

@@ -73,7 +73,6 @@ 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)> {
@@ -87,9 +86,6 @@ 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?;
@@ -137,7 +133,6 @@ impl Database {
&self,
id: DevServerId,
name: &str,
ssh_connection_string: Option<&str>,
user_id: UserId,
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
@@ -150,9 +145,6 @@ impl Database {
dev_server::Entity::update(dev_server::ActiveModel {
name: ActiveValue::Set(name.trim().to_string()),
ssh_connection_string: ActiveValue::Set(
ssh_connection_string.map(ToOwned::to_owned),
),
..dev_server.clone().into_active_model()
})
.exec(&*tx)

View File

@@ -251,7 +251,7 @@ impl Database {
.await?;
let mut is_participant = false;
let mut participant_connection_ids = HashSet::default();
let mut participant_connection_ids = Vec::new();
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.insert(row.connection());
participant_connection_ids.push(row.connection());
}
drop(rows);
@@ -336,9 +336,13 @@ 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,
})
})

View File

@@ -48,9 +48,6 @@ 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()

View File

@@ -10,7 +10,6 @@ pub struct Model {
pub name: String,
pub user_id: UserId,
pub hashed_token: String,
pub ssh_connection_string: Option<String>,
}
impl ActiveModelBehavior for ActiveModel {}
@@ -33,7 +32,6 @@ 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(),
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::{
db::{
tests::{channel_tree, new_test_connection, new_test_user},
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
},
test_both_dbs,
};
@@ -40,15 +40,15 @@ async fn test_channels(db: &Arc<Database>) {
.await
.unwrap();
let (members, _) = db
.get_channel_participant_details(replace_id, "", 10, a_id)
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
})
.await
.unwrap();
let ids = members
.into_iter()
.map(|m| UserId::from_proto(m.user_id))
.collect::<Vec<_>>();
assert_eq!(ids, &[a_id, b_id]);
members.sort();
assert_eq!(members, &[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, "", 100, user_1)
let mut members = db
.get_channel_participant_details(channel_1_1, 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, "", 100, user_1)
let members = db
.get_channel_participant_details(channel_1_3, 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_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(),
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Admin.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, "", 100, admin)
let mut members = db
.get_channel_participant_details(public_channel_id, 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, "", 100, admin)
let mut members = db
.get_channel_participant_details(public_channel_id, 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, "", 100, admin)
let mut members = db
.get_channel_participant_details(public_channel_id, 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, "", 100, admin)
let mut members = db
.get_channel_participant_details(public_channel_id, admin)
.await
.unwrap();

View File

@@ -2365,12 +2365,7 @@ async fn create_dev_server(
let (dev_server, status) = session
.db()
.await
.create_dev_server(
&request.name,
request.ssh_connection_string.as_deref(),
&hashed_access_token,
session.user_id(),
)
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
@@ -2378,7 +2373,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,
name: request.name.clone(),
})?;
Ok(())
}
@@ -2439,12 +2434,7 @@ async fn rename_dev_server(
let status = session
.db()
.await
.rename_dev_server(
dev_server_id,
&request.name,
request.ssh_connection_string.as_deref(),
session.user_id(),
)
.rename_dev_server(dev_server_id, &request.name, session.user_id())
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
@@ -3693,15 +3683,10 @@ async fn get_channel_members(
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_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())
let members = db
.get_channel_participant_details(channel_id, session.user_id())
.await?;
response.send(proto::GetChannelMembersResponse { members, users })?;
response.send(proto::GetChannelMembersResponse { members })?;
Ok(())
}
@@ -3901,13 +3886,13 @@ async fn update_channel_buffer(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let (collaborators, epoch, version) = db
let (collaborators, non_collaborators, epoch, version) = db
.update_channel_buffer(channel_id, session.user_id(), &request.operations)
.await?;
channel_buffer_updated(
session.connection_id,
collaborators.clone(),
collaborators,
&proto::UpdateChannelBuffer {
channel_id: channel_id.to_proto(),
operations: request.operations,
@@ -3917,29 +3902,25 @@ async fn update_channel_buffer(
let pool = &*session.connection_pool().await;
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()
},
)
});
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()
},
)
},
);
Ok(())
}
@@ -4067,6 +4048,7 @@ async fn send_channel_message(
let CreatedChannelMessage {
message_id,
participant_connection_ids,
channel_members,
notifications,
} = session
.db()
@@ -4097,7 +4079,7 @@ async fn send_channel_message(
};
broadcast(
Some(session.connection_id),
participant_connection_ids.clone(),
participant_connection_ids,
|connection| {
session.peer.send(
connection,
@@ -4113,27 +4095,24 @@ async fn send_channel_message(
})?;
let pool = &*session.connection_pool().await;
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()
},
)
});
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()
},
)
},
);
send_notifications(pool, &session.peer, notifications);
Ok(())

View File

@@ -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.fuzzy_search_members(channel_a_id, "".to_string(), 10, cx)
store.get_channel_member_details(channel_a_id, cx)
})
.await
.unwrap();

View File

@@ -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(), None, cx)
store.create_dev_server("server-1".to_string(), 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(), None, cx)
store.create_dev_server("server-1".to_string(), cx)
})
.await
.unwrap();
@@ -352,7 +352,6 @@ async fn test_dev_server_rename(
store.rename_dev_server(
store.dev_servers().first().unwrap().id,
"name-edited".to_string(),
None,
cx,
)
})
@@ -522,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(), None, cx)
store.create_dev_server("server-2".to_string(), cx)
})
.await
.unwrap();

View File

@@ -19,7 +19,7 @@ use editor::{
};
use futures::StreamExt;
use git::diff::DiffHunkStatus;
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use gpui::{BorrowAppContext, TestAppContext, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});

View File

@@ -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, Model, Modifiers, MouseButton, MouseDownEvent,
TestAppContext, UpdateGlobal,
px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
MouseDownEvent, TestAppContext,
};
use language::{
language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
language_settings::{AllLanguageSettings, Formatter},
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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::External {
command: "awk".into(),
@@ -4445,17 +4445,18 @@ async fn test_prettier_formatting_buffer(
client_a.language_registry().add(Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
path_suffixes: vec!["rs".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(
"TypeScript",
"Rust",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
@@ -4469,11 +4470,11 @@ async fn test_prettier_formatting_buffer(
let buffer_text = "let one = \"two\"";
client_a
.fs()
.insert_tree(&directory, json!({ "a.ts": buffer_text }))
.insert_tree(&directory, json!({ "a.rs": 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.ts"), cx));
let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
let project_id = active_call_a
@@ -4481,28 +4482,20 @@ 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.ts"), cx));
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, 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| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::LanguageServer);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});

View File

@@ -34,6 +34,7 @@ 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

View File

@@ -78,14 +78,12 @@ 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(),
user_store.clone(),
None,
channel_store.clone(),
cx.new_view(|cx| Editor::auto_height(4, cx)),
cx,
)
@@ -233,12 +231,19 @@ 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) {
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 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();
});
};
let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx);

View File

@@ -1,12 +1,12 @@
use anyhow::Result;
use channel::{ChannelChat, ChannelStore, MessageParams};
use client::{UserId, UserStore};
use collections::HashSet;
use channel::{ChannelMembership, ChannelStore, MessageParams};
use client::{ChannelId, UserId};
use collections::{HashMap, HashSet};
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
@@ -31,10 +31,11 @@ lazy_static! {
pub struct MessageEditor {
pub editor: View<Editor>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
channel_store: Model<ChannelStore>,
channel_members: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
reply_to_message_id: Option<u64>,
edit_message_id: Option<u64>,
}
@@ -75,24 +76,12 @@ impl CompletionProvider for MessageEditorCompletionProvider {
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Model<Buffer>,
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_cx: &mut ViewContext<Editor>,
) -> bool {
text == "@"
}
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
channel_store: Model<ChannelStore>,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
@@ -138,8 +127,9 @@ impl MessageEditor {
Self {
editor,
user_store,
channel_chat,
channel_store,
channel_members: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
reply_to_message_id: None,
@@ -171,13 +161,12 @@ impl MessageEditor {
self.edit_message_id = None;
}
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());
pub fn set_channel(
&mut self,
channel_id: ChannelId,
channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{channel_name}"), cx);
@@ -185,6 +174,31 @@ 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 {
@@ -354,19 +368,13 @@ impl MessageEditor {
let start_anchor = buffer.read(cx).anchor_before(start_offset);
let mut names = HashSet::default();
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)
{
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) {
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
@@ -473,15 +481,11 @@ impl MessageEditor {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix('@') {
if let Some(user) = this
.user_store
.read(cx)
.cached_user_by_github_login(username)
{
if let Some(user_id) = this.channel_members.get(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);
}
}
@@ -546,3 +550,106 @@ 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
}
}

View File

@@ -1569,28 +1569,11 @@ impl CollabPanel {
*pending_name = Some(channel_name.clone());
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,
)
})
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.create_channel(&channel_name, *location, cx)
})
.detach_and_prompt_err(
"Failed to create channel",
cx,
|_, _| None,
);
} else {
create.detach_and_prompt_err("Failed to create channel", cx, |_, _| None);
}
.detach();
cx.notify();
}
ChannelEditingState::Rename {
@@ -1876,8 +1859,12 @@ 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(
@@ -1885,6 +1872,7 @@ impl CollabPanel {
channel_store.clone(),
channel_id,
mode,
members,
cx,
)
});
@@ -2961,7 +2949,7 @@ struct DraggedChannelView {
}
impl Render for DraggedChannelView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
h_flex()
.font_family(ui_font)

View File

@@ -37,6 +37,7 @@ 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();
@@ -53,8 +54,7 @@ impl ChannelModal {
channel_id,
match_candidates: Vec::new(),
context_menu: None,
members: Vec::new(),
has_all_members: false,
members,
mode,
},
cx,
@@ -78,15 +78,37 @@ impl ChannelModal {
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
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()
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();
}
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
@@ -238,7 +260,6 @@ pub struct ChannelModalDelegate {
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
has_all_members: bool,
context_menu: Option<(View<ContextMenu>, Subscription)>,
}
@@ -267,59 +288,37 @@ impl PickerDelegate for ChannelModalDelegate {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
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(())
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(),
}
.log_err()
.await;
})
}
}));
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();
})
}
Mode::InviteMembers => {
let search_users = self

View File

@@ -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,11 +743,7 @@ impl CollabTitlebarItem {
h_flex()
.gap_0p5()
.child(Avatar::new(user.avatar_uri.clone()))
.child(
Icon::new(IconName::ChevronDown)
.size(IconSize::Small)
.color(Color::Muted),
),
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@@ -759,18 +755,16 @@ 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)
.size(IconSize::Small)
.color(Color::Muted),
),
h_flex()
.gap_0p5()
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),

View File

@@ -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, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@@ -362,7 +362,7 @@ impl PickerDelegate for CommandPaletteDelegate {
self.matches.clear();
self.commands.clear();
HitCounts::update_global(cx, |hit_counts, _cx| {
cx.update_global(|hit_counts: &mut HitCounts, _| {
*hit_counts.0.entry(command.name).or_default() += 1;
});
let action = command.action;

View File

@@ -59,10 +59,6 @@ impl CopilotCompletionProvider {
}
impl InlineCompletionProvider for CopilotCompletionProvider {
fn name() -> &'static str {
"copilot"
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -190,23 +186,17 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
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(),
);
}
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_copilot_event(
Some(completion.uuid.clone()),
true,
self.file_extension.clone(),
);
}
}
}
fn discard(
&mut self,
should_report_inline_completion_event: bool,
cx: &mut ModelContext<Self>,
) {
fn discard(&mut self, cx: &mut ModelContext<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None);
@@ -220,17 +210,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
copilot.discard_completions(&self.completions, cx)
})
.detach_and_log_err(cx);
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(),
);
}
}
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_copilot_event(None, false, self.file_extension.clone());
}
}
@@ -292,7 +273,7 @@ mod tests {
};
use fs::FakeFs;
use futures::StreamExt;
use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};
use gpui::{BackgroundExecutor, BorrowAppContext, Context, TestAppContext};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
@@ -492,8 +473,8 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// AcceptInlineCompletion when there is an active suggestion inserts it.
editor.accept_inline_completion(&Default::default(), cx);
// Tabbing when there is an active suggestion inserts it.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
@@ -550,8 +531,8 @@ mod tests {
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Using AcceptInlineCompletion again accepts the suggestion.
editor.accept_inline_completion(&Default::default(), cx);
// Tabbing again accepts the suggestion.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
@@ -1157,7 +1138,7 @@ mod tests {
editor::init_settings(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
});

View File

@@ -39,7 +39,6 @@ impl From<proto::DevServerProject> for DevServerProject {
pub struct DevServer {
pub id: DevServerId,
pub name: SharedString,
pub ssh_connection_string: Option<SharedString>,
pub status: DevServerStatus,
}
@@ -49,7 +48,6 @@ 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()),
}
}
}
@@ -166,17 +164,11 @@ 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,
ssh_connection_string,
})
.await?;
let result = client.request(proto::CreateDevServer { name }).await?;
Ok(result)
})
}
@@ -185,7 +177,6 @@ impl Store {
&mut self,
dev_server_id: DevServerId,
name: String,
ssh_connection_string: Option<String>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
@@ -194,7 +185,6 @@ impl Store {
.request(proto::RenameDevServer {
dev_server_id: dev_server_id.0,
name,
ssh_connection_string,
})
.await?;
Ok(())

View File

@@ -87,7 +87,7 @@ struct DiagnosticGroupState {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
let child = if self.path_states.is_empty() {
div()
.bg(cx.theme().colors().editor_background)

View File

@@ -19,7 +19,6 @@ test-support = [
"gpui/test-support",
"multi_buffer/test-support",
"project/test-support",
"theme/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
@@ -87,7 +86,6 @@ release_channel.workspace = true
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true

View File

@@ -143,7 +143,6 @@ gpui::actions!(
editor,
[
AcceptPartialCopilotSuggestion,
AcceptInlineCompletion,
AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,

View File

@@ -18,7 +18,6 @@
//! [EditorElement]: crate::element::EditorElement
mod block_map;
mod flap_map;
mod fold_map;
mod inlay_map;
mod tab_map;
@@ -29,9 +28,7 @@ use crate::{EditorStyle, RowExt};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle};
use inlay_map::InlayMap;
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
@@ -45,7 +42,6 @@ use serde::Deserialize;
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use ui::WindowContext;
use wrap_map::WrapMap;
@@ -53,15 +49,10 @@ pub use block_map::{
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
pub use flap_map::*;
use self::block_map::{BlockRow, BlockSnapshot};
use self::fold_map::FoldSnapshot;
use self::block_map::BlockRow;
pub use self::fold_map::{Fold, FoldId, FoldPoint};
use self::inlay_map::InlaySnapshot;
pub use self::inlay_map::{InlayOffset, InlayPoint};
use self::tab_map::TabSnapshot;
use self::wrap_map::WrapSnapshot;
pub(crate) use inlay_map::Inlay;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -70,8 +61,6 @@ pub enum FoldStatus {
Foldable,
}
pub type RenderFoldToggle = Arc<dyn Fn(FoldStatus, &mut WindowContext) -> AnyElement>;
const UNNECESSARY_CODE_FADE: f32 = 0.3;
pub trait ToDisplayPoint {
@@ -103,8 +92,6 @@ pub struct DisplayMap {
text_highlights: TextHighlights,
/// Regions of inlays that should be highlighted.
inlay_highlights: InlayHighlights,
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
flap_map: FlapMap,
pub clip_at_line_ends: bool,
}
@@ -126,9 +113,7 @@ impl DisplayMap {
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
let flap_map = FlapMap::default();
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
buffer_subscription,
@@ -137,7 +122,6 @@ impl DisplayMap {
tab_map,
wrap_map,
block_map,
flap_map,
text_highlights: Default::default(),
inlay_highlights: Default::default(),
clip_at_line_ends: false,
@@ -163,7 +147,6 @@ impl DisplayMap {
tab_snapshot,
wrap_snapshot,
block_snapshot,
flap_snapshot: self.flap_map.snapshot(),
text_highlights: self.text_highlights.clone(),
inlay_highlights: self.inlay_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
@@ -174,14 +157,14 @@ impl DisplayMap {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)),
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
cx,
);
}
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
ranges: impl IntoIterator<Item = Range<T>>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -226,24 +209,6 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn insert_flaps(
&mut self,
flaps: impl IntoIterator<Item = Flap>,
cx: &mut ModelContext<Self>,
) -> Vec<FlapId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.flap_map.insert(flaps, &snapshot)
}
pub fn remove_flaps(
&mut self,
flap_ids: impl IntoIterator<Item = FlapId>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.flap_map.remove(flap_ids, &snapshot)
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -402,12 +367,11 @@ pub struct HighlightedChunk<'a> {
#[derive(Clone)]
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: FoldSnapshot,
pub flap_snapshot: FlapSnapshot,
inlay_snapshot: InlaySnapshot,
tab_snapshot: TabSnapshot,
wrap_snapshot: WrapSnapshot,
block_snapshot: BlockSnapshot,
pub fold_snapshot: fold_map::FoldSnapshot,
inlay_snapshot: inlay_map::InlaySnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
text_highlights: TextHighlights,
inlay_highlights: InlayHighlights,
clip_at_line_ends: bool,
@@ -869,7 +833,17 @@ impl DisplaySnapshot {
DisplayRow(self.block_snapshot.longest_row())
}
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
pub fn fold_for_line(&self, buffer_row: MultiBufferRow) -> Option<FoldStatus> {
if self.is_line_folded(buffer_row) {
Some(FoldStatus::Folded)
} else if self.is_foldable(buffer_row) {
Some(FoldStatus::Foldable)
} else {
None
}
}
pub fn is_foldable(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_buffer_row();
if buffer_row >= max_row {
return false;
@@ -893,17 +867,9 @@ impl DisplaySnapshot {
false
}
pub fn foldable_range(
&self,
buffer_row: MultiBufferRow,
) -> Option<(Range<Point>, &'static str)> {
pub fn foldable_range(&self, buffer_row: MultiBufferRow) -> Option<Range<Point>> {
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
if let Some(flap) = self
.flap_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
Some((flap.range.to_point(&self.buffer_snapshot), ""))
} else if self.starts_indent(MultiBufferRow(start.row))
if self.is_foldable(MultiBufferRow(start.row))
&& !self.is_line_folded(MultiBufferRow(start.row))
{
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
@@ -922,7 +888,7 @@ impl DisplaySnapshot {
}
}
let end = end.unwrap_or(max_point);
Some((start..end, ""))
Some(start..end)
} else {
None
}
@@ -1199,7 +1165,7 @@ pub mod tests {
} else {
log::info!("folding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.fold(ranges.into_iter().map(|range| (range, "")), cx);
map.fold(ranges, cx);
});
}
}
@@ -1547,10 +1513,7 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
)],
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
cx,
)
});
@@ -1632,10 +1595,7 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
)],
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
cx,
)
});
@@ -1794,33 +1754,6 @@ pub mod tests {
assert("αˇ", cx);
}
#[gpui::test]
fn test_flaps(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
let buffer = MultiBuffer::build_simple(text, cx);
let font_size = px(14.0);
cx.new_model(|cx| {
let mut map =
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx);
let snapshot = map.buffer.read(cx).snapshot(cx);
let range =
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
map.flap_map.insert(
[Flap::new(
range,
|_row, _status, _toggle, _cx| div(),
|_row, _status, _cx| div(),
)],
&map.buffer.read(cx).snapshot(cx),
);
map
});
}
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});

View File

@@ -1,292 +0,0 @@
use collections::HashMap;
use gpui::{AnyElement, IntoElement};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point;
use ui::WindowContext;
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct FlapId(usize);
#[derive(Default)]
pub struct FlapMap {
snapshot: FlapSnapshot,
next_id: FlapId,
id_to_range: HashMap<FlapId, Range<Anchor>>,
}
#[derive(Clone, Default)]
pub struct FlapSnapshot {
flaps: SumTree<FlapItem>,
}
impl FlapSnapshot {
/// Returns the first Flap starting on the specified buffer row.
pub fn query_row<'a>(
&'a self,
row: MultiBufferRow,
snapshot: &'a MultiBufferSnapshot,
) -> Option<&'a Flap> {
let start = snapshot.anchor_before(Point::new(row.0, 0));
let mut cursor = self.flaps.cursor::<ItemSummary>();
cursor.seek(&start, Bias::Left, snapshot);
while let Some(item) = cursor.item() {
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
Ordering::Equal => return Some(&item.flap),
Ordering::Greater => break,
}
}
return None;
}
pub fn flap_items_with_offsets(
&self,
snapshot: &MultiBufferSnapshot,
) -> Vec<(FlapId, Range<Point>)> {
let mut cursor = self.flaps.cursor::<ItemSummary>();
let mut results = Vec::new();
cursor.next(snapshot);
while let Some(item) = cursor.item() {
let start_point = item.flap.range.start.to_point(snapshot);
let end_point = item.flap.range.end.to_point(snapshot);
results.push((item.id, start_point..end_point));
cursor.next(snapshot);
}
results
}
}
type RenderToggleFn = Arc<
dyn Send
+ Sync
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
) -> AnyElement,
>;
type RenderTrailerFn =
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
#[derive(Clone)]
pub struct Flap {
pub range: Range<Anchor>,
pub render_toggle: RenderToggleFn,
pub render_trailer: RenderTrailerFn,
}
impl Flap {
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
range: Range<Anchor>,
render_toggle: RenderToggle,
render_trailer: RenderTrailer,
) -> Self
where
RenderToggle: 'static
+ Send
+ Sync
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
) -> ToggleElement
+ 'static,
ToggleElement: IntoElement,
RenderTrailer: 'static
+ Send
+ Sync
+ Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement
+ 'static,
TrailerElement: IntoElement,
{
Flap {
range,
render_toggle: Arc::new(move |row, folded, toggle, cx| {
render_toggle(row, folded, toggle, cx).into_any_element()
}),
render_trailer: Arc::new(move |row, folded, cx| {
render_trailer(row, folded, cx).into_any_element()
}),
}
}
}
impl std::fmt::Debug for Flap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Flap").field("range", &self.range).finish()
}
}
#[derive(Clone, Debug)]
struct FlapItem {
id: FlapId,
flap: Flap,
}
impl FlapMap {
pub fn snapshot(&self) -> FlapSnapshot {
self.snapshot.clone()
}
pub fn insert(
&mut self,
flaps: impl IntoIterator<Item = Flap>,
snapshot: &MultiBufferSnapshot,
) -> Vec<FlapId> {
let mut new_ids = Vec::new();
self.snapshot.flaps = {
let mut new_flaps = SumTree::new();
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
for flap in flaps {
new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot);
let id = self.next_id;
self.next_id.0 += 1;
self.id_to_range.insert(id, flap.range.clone());
new_flaps.push(FlapItem { flap, id }, snapshot);
new_ids.push(id);
}
new_flaps.append(cursor.suffix(snapshot), snapshot);
new_flaps
};
new_ids
}
pub fn remove(
&mut self,
ids: impl IntoIterator<Item = FlapId>,
snapshot: &MultiBufferSnapshot,
) {
let mut removals = Vec::new();
for id in ids {
if let Some(range) = self.id_to_range.remove(&id) {
removals.push((id, range.clone()));
}
}
removals.sort_unstable_by(|(a_id, a_range), (b_id, b_range)| {
AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id))
});
self.snapshot.flaps = {
let mut new_flaps = SumTree::new();
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
for (id, range) in removals {
new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
while let Some(item) = cursor.item() {
cursor.next(snapshot);
if item.id == id {
break;
} else {
new_flaps.push(item.clone(), snapshot);
}
}
}
new_flaps.append(cursor.suffix(snapshot), snapshot);
new_flaps
};
}
}
#[derive(Debug, Clone)]
pub struct ItemSummary {
range: Range<Anchor>,
}
impl Default for ItemSummary {
fn default() -> Self {
Self {
range: Anchor::min()..Anchor::min(),
}
}
}
impl sum_tree::Summary for ItemSummary {
type Context = MultiBufferSnapshot;
fn add_summary(&mut self, other: &Self, _snapshot: &MultiBufferSnapshot) {
self.range = other.range.clone();
}
}
impl sum_tree::Item for FlapItem {
type Summary = ItemSummary;
fn summary(&self) -> Self::Summary {
ItemSummary {
range: self.flap.range.clone(),
}
}
}
/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
AnchorRangeExt::cmp(self, &cursor_location.range, snapshot)
}
}
impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor {
fn cmp(&self, other: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
self.cmp(&other.range.start, snapshot)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{div, AppContext};
use multi_buffer::MultiBuffer;
#[gpui::test]
fn test_insert_and_remove_flaps(cx: &mut AppContext) {
let text = "line1\nline2\nline3\nline4\nline5";
let buffer = MultiBuffer::build_simple(text, cx);
let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let mut flap_map = FlapMap::default();
// Insert flaps
let flaps = [
Flap::new(
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Flap::new(
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
];
let flap_ids = flap_map.insert(flaps, &snapshot);
assert_eq!(flap_ids.len(), 2);
// Verify flaps are inserted
let flap_snapshot = flap_map.snapshot();
assert!(flap_snapshot
.query_row(MultiBufferRow(1), &snapshot)
.is_some());
assert!(flap_snapshot
.query_row(MultiBufferRow(3), &snapshot)
.is_some());
// Remove flaps
flap_map.remove(flap_ids, &snapshot);
// Verify flaps are removed
let flap_snapshot = flap_map.snapshot();
assert!(flap_snapshot
.query_row(MultiBufferRow(1), &snapshot)
.is_none());
assert!(flap_snapshot
.query_row(MultiBufferRow(3), &snapshot)
.is_none());
}
}

View File

@@ -75,12 +75,12 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> {
pub(crate) fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
ranges: impl IntoIterator<Item = Range<T>>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
for (range, fold_text) in ranges.into_iter() {
for range in ranges.into_iter() {
let buffer = &snapshot.buffer;
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
@@ -99,7 +99,6 @@ impl<'a> FoldMapWriter<'a> {
folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
text: fold_text,
});
let inlay_range =
@@ -325,14 +324,11 @@ impl FoldMap {
let mut folds = iter::from_fn({
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|fold| {
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
(
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end),
fold.text,
)
let item = folds_cursor.item().map(|f| {
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end)
});
folds_cursor.next(&inlay_snapshot.buffer);
item
@@ -340,27 +336,25 @@ impl FoldMap {
})
.peekable();
while folds
.peek()
.map_or(false, |(fold_range, _)| fold_range.start < edit.new.end)
{
let (mut fold_range, fold_text) = folds.next().unwrap();
while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
let mut fold = folds.next().unwrap();
let sum = new_transforms.summary();
assert!(fold_range.start.0 >= sum.input.len);
assert!(fold.start.0 >= sum.input.len);
while folds.peek().map_or(false, |(next_fold_range, _)| {
next_fold_range.start <= fold_range.end
}) {
let (next_fold_range, _) = folds.next().unwrap();
if next_fold_range.end > fold_range.end {
fold_range.end = next_fold_range.end;
while folds
.peek()
.map_or(false, |next_fold| next_fold.start <= fold.end)
{
let next_fold = folds.next().unwrap();
if next_fold.end > fold.end {
fold.end = next_fold.end;
}
}
if fold_range.start.0 > sum.input.len {
if fold.start.0 > sum.input.len {
let text_summary = inlay_snapshot
.text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start);
.text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -373,15 +367,16 @@ impl FoldMap {
);
}
if fold_range.end > fold_range.start {
if fold.end > fold.start {
let output_text = "";
new_transforms.push(
Transform {
summary: TransformSummary {
output: TextSummary::from(fold_text),
output: TextSummary::from(output_text),
input: inlay_snapshot
.text_summary_for_range(fold_range.start..fold_range.end),
.text_summary_for_range(fold.start..fold.end),
},
output_text: Some(fold_text),
output_text: Some(output_text),
},
&(),
);
@@ -858,7 +853,6 @@ impl Into<ElementId> for FoldId {
pub struct Fold {
pub id: FoldId,
pub range: FoldRange,
pub text: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -954,7 +948,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
AnchorRangeExt::cmp(&self.0, &other.0, buffer)
self.0.cmp(&other.0, buffer)
}
}
@@ -1165,8 +1159,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
let (snapshot2, edits) = writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(2, 4)..Point::new(4, 1), ""),
Point::new(0, 2)..Point::new(2, 2),
Point::new(2, 4)..Point::new(4, 1),
]);
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
assert_eq!(
@@ -1245,19 +1239,19 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(5..8, "")]);
writer.fold(vec![5..8]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "abcde⋯ijkl");
// Create an fold adjacent to the start of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(0..1, ""), (2..5, "")]);
writer.fold(vec![0..1, 2..5]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
// Create an fold adjacent to the end of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(11..11, ""), (8..10, "")]);
writer.fold(vec![11..11, 8..10]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯kl");
}
@@ -1267,7 +1261,7 @@ mod tests {
// Create two adjacent folds.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(0..2, ""), (2..5, "")]);
writer.fold(vec![0..2, 2..5]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "⋯fghijkl");
@@ -1291,10 +1285,10 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
Point::new(0, 2)..Point::new(2, 2),
Point::new(0, 4)..Point::new(1, 0),
Point::new(1, 2)..Point::new(3, 2),
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯eeeee");
@@ -1311,8 +1305,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
Point::new(0, 2)..Point::new(2, 2),
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
@@ -1336,10 +1330,10 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
Point::new(0, 2)..Point::new(2, 2),
Point::new(0, 4)..Point::new(1, 0),
Point::new(1, 2)..Point::new(3, 2),
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot
@@ -1414,10 +1408,10 @@ mod tests {
snapshot_edits.push((snapshot.clone(), edits));
let mut expected_text: String = inlay_snapshot.text().to_string();
for (fold_range, fold_text) in map.merged_folds().into_iter().rev() {
for fold_range in map.merged_fold_ranges().into_iter().rev() {
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "");
}
assert_eq!(snapshot.text(), expected_text);
@@ -1429,7 +1423,7 @@ mod tests {
let mut prev_row = 0;
let mut expected_buffer_rows = Vec::new();
for (fold_range, _fold_text) in map.merged_folds().into_iter() {
for fold_range in map.merged_fold_ranges().into_iter() {
let fold_start = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
.row();
@@ -1541,11 +1535,11 @@ mod tests {
}
let folded_buffer_rows = map
.merged_folds()
.merged_fold_ranges()
.iter()
.flat_map(|(fold_range, _)| {
let start_row = fold_range.start.to_point(&buffer_snapshot).row;
let end = fold_range.end.to_point(&buffer_snapshot);
.flat_map(|range| {
let start_row = range.start.to_point(&buffer_snapshot).row;
let end = range.end.to_point(&buffer_snapshot);
if end.column == 0 {
start_row..end.row
} else {
@@ -1640,8 +1634,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
Point::new(0, 2)..Point::new(2, 2),
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
@@ -1659,39 +1653,34 @@ mod tests {
}
impl FoldMap {
fn merged_folds(&self) -> Vec<(Range<usize>, &'static str)> {
fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
let mut folds = folds
let mut fold_ranges = folds
.iter()
.map(|fold| {
(
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer),
fold.text,
)
})
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
.peekable();
let mut merged_folds = Vec::new();
while let Some((mut fold_range, fold_text)) = folds.next() {
while let Some((next_range, _)) = folds.peek() {
let mut merged_ranges = Vec::new();
while let Some(mut fold_range) = fold_ranges.next() {
while let Some(next_range) = fold_ranges.peek() {
if fold_range.end >= next_range.start {
if next_range.end > fold_range.end {
fold_range.end = next_range.end;
}
folds.next();
fold_ranges.next();
} else {
break;
}
}
if fold_range.end > fold_range.start {
merged_folds.push((fold_range, fold_text));
merged_ranges.push(fold_range);
}
}
merged_folds
merged_ranges
}
pub fn randomly_mutate(
@@ -1709,11 +1698,10 @@ mod tests {
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
to_unfold.push(start..end);
}
let inclusive = rng.gen();
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
log::info!("unfolding {:?}", to_unfold);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
snapshot_edits.push((snapshot, edits));
let (snapshot, edits) = writer.unfold(to_unfold, inclusive);
let (snapshot, edits) = writer.fold(to_unfold);
snapshot_edits.push((snapshot, edits));
}
_ => {
@@ -1723,8 +1711,7 @@ mod tests {
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
let text = if rng.gen() { "" } else { "" };
to_fold.push((start..end, text));
to_fold.push(start..end);
}
log::info!("folding {:?}", to_fold);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,10 @@ use crate::{
JoinLines,
};
use futures::StreamExt;
use gpui::{div, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions};
use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
use indoc::indoc;
use language::{
language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
},
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
BracketPairConfig,
Capability::ReadWrite,
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
@@ -494,8 +492,8 @@ fn test_clone(cx: &mut TestAppContext) {
editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
editor.fold_ranges(
[
(Point::new(1, 0)..Point::new(2, 0), ""),
(Point::new(3, 0)..Point::new(4, 0), ""),
Point::new(1, 0)..Point::new(2, 0),
Point::new(3, 0)..Point::new(4, 0),
],
true,
cx,
@@ -903,9 +901,9 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
(Point::new(0, 6)..Point::new(0, 12), ""),
(Point::new(1, 2)..Point::new(1, 4), ""),
(Point::new(2, 4)..Point::new(2, 8), ""),
Point::new(0, 6)..Point::new(0, 12),
Point::new(1, 2)..Point::new(1, 4),
Point::new(2, 4)..Point::new(2, 8),
],
true,
cx,
@@ -3407,9 +3405,9 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
(Point::new(0, 2)..Point::new(1, 2), ""),
(Point::new(2, 3)..Point::new(4, 1), ""),
(Point::new(7, 0)..Point::new(8, 4), ""),
Point::new(0, 2)..Point::new(1, 2),
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
],
true,
cx,
@@ -3891,9 +3889,9 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
(Point::new(0, 2)..Point::new(1, 2), ""),
(Point::new(2, 3)..Point::new(4, 1), ""),
(Point::new(7, 0)..Point::new(8, 4), ""),
Point::new(0, 2)..Point::new(1, 2),
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
],
true,
cx,
@@ -4548,8 +4546,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
(Point::new(0, 21)..Point::new(0, 24), ""),
(Point::new(3, 20)..Point::new(3, 22), ""),
Point::new(0, 21)..Point::new(0, 24),
Point::new(3, 20)..Point::new(3, 22),
],
true,
cx,
@@ -6256,18 +6254,13 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..LanguageConfig::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()
},
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 {
@@ -6531,7 +6524,6 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx,
)
.await;
let counter = Arc::new(AtomicUsize::new(0));
cx.set_state(indoc! {"
oneˇ
@@ -6547,13 +6539,10 @@ 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
@@ -6624,12 +6613,10 @@ 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");
@@ -6642,12 +6629,10 @@ 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
@@ -6682,17 +6667,9 @@ 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"],
counter.clone(),
)
.await;
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).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)
@@ -6703,103 +6680,6 @@ 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, |_| {});
@@ -8719,32 +8599,27 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
});
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.ts", Default::default()).await;
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
path_suffixes: vec!["rs".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(
"TypeScript",
"Rust",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
@@ -8753,7 +8628,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.ts", cx))
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
@@ -11448,67 +11323,6 @@ async fn test_multiple_expanded_hunks_merge(
);
}
#[gpui::test]
fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
build_editor(buffer, cx)
});
let render_args = Arc::new(Mutex::new(None));
let snapshot = editor
.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let range =
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
struct RenderArgs {
row: MultiBufferRow,
folded: bool,
callback: Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
}
let flap = Flap::new(
range,
{
let toggle_callback = render_args.clone();
move |row, folded, callback, _cx| {
*toggle_callback.lock() = Some(RenderArgs {
row,
folded,
callback,
});
div()
}
},
|_row, _folded, _cx| div(),
);
editor.insert_flaps(Some(flap), cx);
let snapshot = editor.snapshot(cx);
let _div = snapshot.render_fold_toggle(MultiBufferRow(1), false, cx.view().clone(), cx);
snapshot
})
.unwrap();
let render_args = render_args.lock().take().unwrap();
assert_eq!(render_args.row, MultiBufferRow(1));
assert_eq!(render_args.folded, false);
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
cx.update_window(*editor, |_, cx| (render_args.callback)(true, cx))
.unwrap();
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
assert!(snapshot.is_line_folded(MultiBufferRow(1)));
cx.update_window(*editor, |_, cx| (render_args.callback)(false, cx))
.unwrap();
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -11532,7 +11346,6 @@ 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();
@@ -11548,7 +11361,6 @@ 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!(
@@ -11612,7 +11424,7 @@ pub(crate) fn update_test_language_settings(
f: impl Fn(&mut AllLanguageSettingsContent),
) {
_ = cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
});
@@ -11623,7 +11435,7 @@ pub(crate) fn update_test_project_settings(
f: impl Fn(&mut ProjectSettings),
) {
_ = cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, f);
});
});

View File

@@ -1,7 +1,8 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
display_map::{
BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock,
BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint,
TransformBlock,
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar,
@@ -25,7 +26,7 @@ use crate::{
};
use anyhow::Result;
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap};
use collections::{BTreeMap, HashMap, HashSet};
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
@@ -50,7 +51,7 @@ use smallvec::SmallVec;
use std::{
any::TypeId,
borrow::Cow,
cmp::{self, Ordering},
cmp::{self, max, Ordering},
fmt::Write,
iter, mem,
ops::{Deref, Range},
@@ -383,7 +384,6 @@ impl EditorElement {
register_action(view, cx, Editor::unique_lines_case_insensitive);
register_action(view, cx, Editor::unique_lines_case_sensitive);
register_action(view, cx, Editor::accept_partial_inline_completion);
register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal)
}
@@ -477,7 +477,6 @@ impl EditorElement {
editor.select(
SelectPhase::BeginColumnar {
position,
reset: false,
goal_column: point_for_position.exact_unclipped.column(),
},
cx,
@@ -538,22 +537,25 @@ impl EditorElement {
text_hitbox: &Hitbox,
cx: &mut ViewContext<Editor>,
) {
if cx.default_prevented() {
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
return;
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
if let Some(item) = cx.read_from_primary() {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
editor.select(
SelectPhase::BeginColumnar {
position,
reset: true,
goal_column: point_for_position.exact_unclipped.column(),
},
cx,
);
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(item.text(), cx);
}
}
fn mouse_up(
@@ -583,28 +585,6 @@ impl EditorElement {
cx.stop_propagation();
} else if end_selection {
cx.stop_propagation();
} else if cfg!(target_os = "linux") && event.button == MouseButton::Middle {
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
return;
}
#[cfg(target_os = "linux")]
if let Some(item) = cx.read_from_clipboard() {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(item.text(), cx);
}
cx.stop_propagation()
}
}
@@ -869,11 +849,6 @@ impl EditorElement {
snapshot
.folds_in_range(visible_anchor_range.clone())
.filter_map(|fold| {
// Skip folds that have no text.
if fold.text.is_empty() {
return None;
}
let fold_range = fold.range.clone();
let display_range = fold.range.start.to_display_point(&snapshot)
..fold.range.end.to_display_point(&snapshot);
@@ -1168,17 +1143,28 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_fold_toggles(
fn layout_gutter_fold_indicators(
&self,
toggles: &mut [Option<AnyElement>],
fold_statuses: Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
gutter_settings: crate::editor_settings::Gutter,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
) {
for (ix, fold_indicator) in toggles.iter_mut().enumerate() {
) -> Vec<Option<AnyElement>> {
let mut indicators = self.editor.update(cx, |editor, cx| {
editor.render_fold_indicators(
fold_statuses,
&self.style,
editor.gutter_hovered,
line_height,
gutter_dimensions.margin,
cx,
)
});
for (ix, fold_indicator) in indicators.iter_mut().enumerate() {
if let Some(fold_indicator) = fold_indicator {
debug_assert!(gutter_settings.folds);
let available_space = size(
@@ -1201,49 +1187,8 @@ impl EditorElement {
fold_indicator.prepaint_as_root(origin, available_space, cx);
}
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_flap_trailers(
&self,
trailers: Vec<Option<AnyElement>>,
lines: &[LineWithInvisibles],
line_height: Pixels,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
em_width: Pixels,
cx: &mut WindowContext,
) -> Vec<Option<FlapTrailerLayout>> {
trailers
.into_iter()
.enumerate()
.map(|(ix, element)| {
let mut element = element?;
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let size = element.layout_as_root(available_space, cx);
let line = &lines[ix].line;
let padding = if line.width == Pixels::ZERO {
Pixels::ZERO
} else {
4. * em_width
};
let position = point(
scroll_pixel_position.x + line.width + padding,
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
);
let centering_offset = point(px(0.), (line_height - size.height) / 2.);
let origin = content_origin + position + centering_offset;
element.prepaint_as_root(origin, available_space, cx);
Some(FlapTrailerLayout {
element,
bounds: Bounds::new(origin, size),
})
})
.collect()
indicators
}
// Folds contained in a hunk are ignored apart from shrinking visual size
@@ -1327,7 +1272,6 @@ impl EditorElement {
display_row: DisplayRow,
display_snapshot: &DisplaySnapshot,
line_layout: &LineWithInvisibles,
flap_trailer: Option<&FlapTrailerLayout>,
em_width: Pixels,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
@@ -1367,22 +1311,17 @@ impl EditorElement {
let start_x = {
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
let line_end = if let Some(flap_trailer) = flap_trailer {
flap_trailer.bounds.right()
} else {
content_origin.x - scroll_pixel_position.x + line_layout.line.width
};
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
let padded_line_width =
line_layout.line.width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS);
let min_column_in_pixels = ProjectSettings::get_global(cx)
let min_column = ProjectSettings::get_global(cx)
.git
.inline_blame
.and_then(|settings| settings.min_column)
.map(|col| self.column_pixels(col as usize, cx))
.unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
cmp::max(padded_line_end, min_start)
(content_origin.x - scroll_pixel_position.x) + max(padded_line_width, min_column)
};
let absolute_offset = point(start_x, start_y);
@@ -1621,16 +1560,13 @@ impl EditorElement {
active_rows: &BTreeMap<DisplayRow, bool>,
newest_selection_head: Option<DisplayPoint>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<ShapedLine>> {
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
});
if !include_line_numbers {
return Vec::new();
}
cx: &WindowContext,
) -> (
Vec<Option<ShapedLine>>,
Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
) {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor.selections.newest::<Point>(cx);
SelectionLayout::new(
@@ -1645,100 +1581,69 @@ impl EditorElement {
.head
});
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let include_line_numbers =
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
let include_fold_statuses =
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
let mut fold_statuses = Vec::with_capacity(rows.len());
let mut line_number = String::new();
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
let mut line_number = String::new();
buffer_rows
.into_iter()
.enumerate()
.map(|(ix, multibuffer_row)| {
let multibuffer_row = multibuffer_row?;
let display_row = DisplayRow(rows.start.0 + ix as u32);
let color = if active_rows.contains_key(&display_row) {
cx.theme().colors().editor_active_line_number
} else {
cx.theme().colors().editor_line_number
};
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
Some(shaped_line)
})
.collect()
}
fn layout_gutter_fold_toggles(
&self,
rows: Range<DisplayRow>,
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
active_rows: &BTreeMap<DisplayRow, bool>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<AnyElement>> {
let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
&& snapshot.mode == EditorMode::Full
&& self.editor.read(cx).is_singleton(cx);
if include_fold_statuses {
buffer_rows
.into_iter()
.enumerate()
.map(|(ix, row)| {
if let Some(multibuffer_row) = row {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let active = active_rows.contains_key(&display_row);
snapshot.render_fold_toggle(
multibuffer_row,
active,
self.editor.clone(),
cx,
)
} else {
None
}
})
.collect()
} else {
Vec::new()
}
}
fn layout_flap_trailers(
&self,
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<AnyElement>> {
buffer_rows
.into_iter()
.map(|row| {
if let Some(multibuffer_row) = row {
snapshot.render_flap_trailer(multibuffer_row, cx)
} else {
None
for (ix, row) in buffer_rows.into_iter().enumerate() {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let (active, color) = if active_rows.contains_key(&display_row) {
(true, cx.theme().colors().editor_active_line_number)
} else {
(false, cx.theme().colors().editor_line_number)
};
if let Some(multibuffer_row) = row {
if include_line_numbers {
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
shaped_line_numbers.push(Some(shaped_line));
}
})
.collect()
if include_fold_statuses {
fold_statuses.push(
is_singleton
.then(|| {
snapshot
.fold_for_line(multibuffer_row)
.map(|fold_status| (fold_status, multibuffer_row, active))
})
.flatten(),
)
}
} else {
fold_statuses.push(None);
shaped_line_numbers.push(None);
}
}
(shaped_line_numbers, fold_statuses)
}
fn layout_lines(
@@ -2513,16 +2418,10 @@ impl EditorElement {
}
}
let show_git_gutter = layout
.position_map
.snapshot
.show_git_diff_gutter
.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
)
});
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
if show_git_gutter {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
@@ -2546,8 +2445,8 @@ impl EditorElement {
}
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
cx.with_element_namespace("gutter_fold_toggles", |cx| {
for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() {
cx.with_element_namespace("gutter_fold_indicators", |cx| {
for fold_indicator in layout.fold_indicators.iter_mut().flatten() {
fold_indicator.paint(cx);
}
});
@@ -2727,11 +2626,6 @@ impl EditorElement {
self.paint_redactions(layout, cx);
self.paint_cursors(layout, cx);
self.paint_inline_blame(layout, cx);
cx.with_element_namespace("flap_trailers", |cx| {
for trailer in layout.flap_trailers.iter_mut().flatten() {
trailer.element.paint(cx);
}
});
},
)
}
@@ -3377,9 +3271,7 @@ impl EditorElement {
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
editor.update(cx, |editor, cx| {
if event.pressed_button == Some(MouseButton::Left)
|| event.pressed_button == Some(MouseButton::Middle)
{
if event.pressed_button == Some(MouseButton::Left) {
Self::mouse_dragged(
editor,
event,
@@ -4054,9 +3946,9 @@ impl Element for EditorElement {
)
};
let highlighted_rows = self
.editor
.update(cx, |editor, cx| editor.highlighted_display_rows(cx));
let highlighted_rows = self.editor.update(cx, |editor, cx| {
editor.highlighted_display_rows(HashSet::default(), cx)
});
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
@@ -4078,29 +3970,15 @@ impl Element for EditorElement {
cx,
);
let line_numbers = self.layout_line_numbers(
let (line_numbers, fold_statuses) = self.layout_line_numbers(
start_row..end_row,
buffer_rows.iter().copied(),
buffer_rows.clone().into_iter(),
&active_rows,
newest_selection_head,
&snapshot,
cx,
);
let mut gutter_fold_toggles =
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.layout_gutter_fold_toggles(
start_row..end_row,
buffer_rows.iter().copied(),
&active_rows,
&snapshot,
cx,
)
});
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
self.layout_flap_trailers(buffer_rows.iter().copied(), &snapshot, cx)
});
let display_hunks = self.layout_git_gutters(
line_height,
&gutter_hitbox,
@@ -4146,30 +4024,15 @@ impl Element for EditorElement {
scroll_position.y * line_height,
);
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
self.prepaint_flap_trailers(
flap_trailers,
&line_layouts,
line_height,
content_origin,
scroll_pixel_position,
em_width,
cx,
)
});
let mut inline_blame = None;
if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row();
if (start_row..end_row).contains(&display_row) {
let line_ix = display_row.minus(start_row) as usize;
let line_layout = &line_layouts[line_ix];
let flap_trailer_layout = flap_trailers[line_ix].as_ref();
let line_layout = &line_layouts[display_row.minus(start_row) as usize];
inline_blame = self.layout_inline_blame(
display_row,
&snapshot.display_snapshot,
line_layout,
flap_trailer_layout,
em_width,
content_origin,
scroll_pixel_position,
@@ -4287,11 +4150,7 @@ impl Element for EditorElement {
gutter_dimensions.width - gutter_dimensions.left_padding,
cx,
);
let show_code_actions = snapshot
.show_code_actions
.unwrap_or_else(|| gutter_settings.code_actions);
if show_code_actions {
if gutter_settings.code_actions {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
@@ -4345,17 +4204,21 @@ impl Element for EditorElement {
let mouse_context_menu = self.layout_mouse_context_menu(cx);
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.prepaint_gutter_fold_toggles(
&mut gutter_fold_toggles,
line_height,
&gutter_dimensions,
gutter_settings,
scroll_pixel_position,
&gutter_hitbox,
cx,
)
});
let fold_indicators = if gutter_settings.folds {
cx.with_element_namespace("gutter_fold_indicators", |cx| {
self.layout_gutter_fold_indicators(
fold_statuses,
line_height,
&gutter_dimensions,
gutter_settings,
scroll_pixel_position,
&gutter_hitbox,
cx,
)
})
} else {
Vec::new()
};
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = cx
@@ -4425,8 +4288,7 @@ impl Element for EditorElement {
mouse_context_menu,
test_indicators,
code_actions_indicator,
gutter_fold_toggles,
flap_trailers,
fold_indicators,
tab_invisible,
space_invisible,
}
@@ -4546,8 +4408,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
flap_trailers: Vec<Option<FlapTrailerLayout>>,
fold_indicators: Vec<Option<AnyElement>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
@@ -4671,11 +4532,6 @@ impl ScrollbarLayout {
}
}
struct FlapTrailerLayout {
element: AnyElement,
bounds: Bounds<Pixels>,
}
struct FoldLayout {
display_range: Range<DisplayPoint>,
hover_element: AnyElement,
@@ -5094,14 +4950,16 @@ mod tests {
let layouts = cx
.update_window(*window, |_, cx| {
element.layout_line_numbers(
DisplayRow(0)..DisplayRow(6),
(0..6).map(MultiBufferRow).map(Some),
&Default::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
cx,
)
element
.layout_line_numbers(
DisplayRow(0)..DisplayRow(6),
(0..6).map(MultiBufferRow).map(Some),
&Default::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
cx,
)
.0
})
.unwrap();
assert_eq!(layouts.len(), 6);

View File

@@ -194,7 +194,6 @@ impl Editor {
editor.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(removed_rows, &snapshot),
None,
false,
cx,
);
}
@@ -270,7 +269,6 @@ impl Editor {
self.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
Some(added_hunk_color(cx)),
false,
cx,
);
None
@@ -279,7 +277,6 @@ 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)
@@ -479,7 +476,6 @@ impl Editor {
editor.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(removed_rows, &snapshot),
None,
false,
cx,
);
}
@@ -585,7 +581,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), false, cx);
editor.highlight_rows::<DiffRowHighlight>(start..=end, Some(deleted_color), cx);
let subscription_editor = parent_editor.clone();
editor._subscriptions.extend([

View File

@@ -3,7 +3,6 @@ use gpui::{AppContext, Model, ModelContext};
use language::Buffer;
pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -25,7 +24,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
cx: &mut ModelContext<Self>,
);
fn accept(&mut self, cx: &mut ModelContext<Self>);
fn discard(&mut self, should_report_inline_completion_event: bool, cx: &mut ModelContext<Self>);
fn discard(&mut self, cx: &mut ModelContext<Self>);
fn active_completion_text<'a>(
&'a self,
buffer: &Model<Buffer>,
@@ -56,7 +55,7 @@ pub trait InlineCompletionProviderHandle {
cx: &mut AppContext,
);
fn accept(&self, cx: &mut AppContext);
fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext);
fn discard(&self, cx: &mut AppContext);
fn active_completion_text<'a>(
&'a self,
buffer: &Model<Buffer>,
@@ -106,10 +105,8 @@ where
self.update(cx, |this, cx| this.accept(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 discard(&self, cx: &mut AppContext) {
self.update(cx, |this, cx| this.discard(cx))
}
fn active_completion_text<'a>(

View File

@@ -1,7 +1,6 @@
use crate::{
Copy, Cut, DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition,
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFinder, SelectMode,
ToggleCodeActions,
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use workspace::OpenInTerminal;
@@ -86,10 +85,6 @@ 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));
match focus {

View File

@@ -1,9 +1,13 @@
use crate::{
display_map::ToDisplayPoint, DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt,
};
use std::{any::TypeId, cmp, f32};
use collections::HashSet;
use gpui::{px, Bounds, Pixels, ViewContext};
use language::Point;
use std::{cmp, f32};
use crate::{
display_map::ToDisplayPoint, DiffRowHighlight, DisplayRow, Editor, EditorMode,
LineWithInvisibles, RowExt,
};
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum Autoscroll {
@@ -103,10 +107,14 @@ impl Editor {
let mut target_top;
let mut target_bottom;
if let Some(first_highlighted_row) =
self.highlighted_display_row_for_autoscroll(&display_map)
if let Some(first_highlighted_row) = &self
.highlighted_display_rows(
HashSet::from_iter(Some(TypeId::of::<DiffRowHighlight>())),
cx,
)
.first_entry()
{
target_top = first_highlighted_row.as_f32();
target_top = first_highlighted_row.key().as_f32();
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(cx);
@@ -236,10 +244,7 @@ impl Editor {
let mut target_left;
let mut target_right;
if self
.highlighted_display_row_for_autoscroll(&display_map)
.is_none()
{
if self.highlighted_rows.is_empty() {
target_left = px(f32::INFINITY);
target_right = px(0.);
for selection in selections {

View File

@@ -1,16 +1,16 @@
use crate::Editor;
use std::{path::Path, sync::Arc};
use anyhow::Context;
use gpui::{Model, WindowContext};
use language::ContextProvider;
use project::{BasicContextProvider, Location, Project};
use gpui::WindowContext;
use language::{BasicContextProvider, ContextProvider};
use project::{Location, WorktreeId};
use task::{TaskContext, TaskVariables, VariableName};
use text::Point;
use util::ResultExt;
use workspace::Workspace;
pub(crate) fn task_context_for_location(
captured_variables: TaskVariables,
workspace: &Workspace,
location: Location,
cx: &mut WindowContext<'_>,
@@ -19,33 +19,44 @@ pub(crate) fn task_context_for_location(
.log_err()
.flatten();
let mut task_variables = combine_task_variables(
captured_variables,
let buffer = location.buffer.clone();
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let worktree_abs_path = buffer
.read(cx)
.file()
.map(|file| WorktreeId::from_usize(file.worktree_id()))
.and_then(|worktree_id| {
workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
});
let task_variables = combine_task_variables(
worktree_abs_path.as_deref(),
location,
workspace.project().clone(),
language_context_provider.as_ref(),
cx,
)
.log_err()?;
// Remove all custom entries starting with _, as they're not intended for use by the end user.
task_variables.sweep();
Some(TaskContext {
cwd,
task_variables,
})
}
fn task_context_with_editor(
pub(crate) fn task_context_with_editor(
workspace: &Workspace,
editor: &mut Editor,
cx: &mut WindowContext<'_>,
) -> Option<TaskContext> {
let (selection, buffer, editor_snapshot) = {
let mut selection = editor.selections.newest::<Point>(cx);
if editor.selections.line_mode {
selection.start = Point::new(selection.start.row, 0);
selection.end = Point::new(selection.end.row + 1, 0);
}
let selection = editor.selections.newest::<usize>(cx);
let (buffer, _, _) = editor
.buffer()
.read(cx)
@@ -68,21 +79,21 @@ fn task_context_with_editor(
buffer,
range: start..end,
};
let captured_variables = {
let mut variables = TaskVariables::default();
task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| {
for range in location
.buffer
.read(cx)
.snapshot()
.runnable_ranges(location.range.clone())
.runnable_ranges(location.range)
{
for (capture_name, value) in range.extra_captures {
variables.insert(VariableName::Custom(capture_name.into()), value);
task_context
.task_variables
.insert(VariableName::Custom(capture_name.into()), value);
}
}
variables
};
task_context_for_location(captured_variables, workspace, location.clone(), cx)
task_context
})
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
@@ -98,26 +109,24 @@ pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskCo
}
fn combine_task_variables(
mut captured_variables: TaskVariables,
worktree_abs_path: Option<&Path>,
location: Location,
project: Model<Project>,
context_provider: &dyn ContextProvider,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
let language_context_provider = location
.buffer
.read(cx)
.language()
.and_then(|language| language.context_provider());
let baseline = BasicContextProvider::new(project)
.build_context(&captured_variables, &location, cx)
.context("building basic default context")?;
captured_variables.extend(baseline);
if let Some(provider) = language_context_provider {
captured_variables.extend(
provider
.build_context(&captured_variables, &location, cx)
if context_provider.is_basic() {
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building basic provider context")
} else {
let mut basic_context = BasicContextProvider
.build_context(worktree_abs_path, &location, cx)
.context("building basic default context")?;
basic_context.extend(
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building provider context ")?,
);
Ok(basic_context)
}
Ok(captured_variables)
}

View File

@@ -168,7 +168,8 @@ 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(cx) {
for (highlighted_row, _) in editor.highlighted_display_rows(collections::HashSet::default(), cx)
{
match previous_highlighted_row {
Some(previous_row) => {
if previous_row + 1 != highlighted_row.0 {

View File

@@ -30,7 +30,6 @@ log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
project.workspace = true
release_channel.workspace = true
schemars.workspace = true
semantic_version.workspace = true
serde.workspace = true

View File

@@ -67,7 +67,7 @@ impl ExtensionBuilder {
pub fn new(cache_dir: PathBuf) -> Self {
Self {
cache_dir,
http: http::client(None),
http: http::client(),
}
}

View File

@@ -30,11 +30,10 @@ use gpui::{
};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -71,10 +70,7 @@ pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
}
/// Returns whether the given extension version is compatible with this version of Zed.
pub fn is_version_compatible(
release_channel: ReleaseChannel,
extension_version: &ExtensionMetadata,
) -> bool {
pub fn is_version_compatible(extension_version: &ExtensionMetadata) -> bool {
let schema_version = extension_version.manifest.schema_version.unwrap_or(0);
if CURRENT_SCHEMA_VERSION.0 < schema_version {
return false;
@@ -86,7 +82,7 @@ pub fn is_version_compatible(
.as_ref()
.and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
{
if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
if !is_supported_wasm_api_version(wasm_api_version) {
return false;
}
}
@@ -432,7 +428,7 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<ExtensionMetadata>>> {
let schema_versions = schema_version_range();
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
let wasm_api_versions = wasm_api_version_range();
let extension_settings = ExtensionSettings::get_global(cx);
let extension_ids = self
.extension_index
@@ -685,7 +681,7 @@ impl ExtensionStore {
log::info!("installing extension {extension_id} latest version");
let schema_versions = schema_version_range();
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
let wasm_api_versions = wasm_api_version_range();
let Some(url) = self
.http_client

View File

@@ -714,7 +714,6 @@ fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
release_channel::init("0.0.0", cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
ExtensionSettings::register(cx);

View File

@@ -16,7 +16,6 @@ use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use http::HttpClient;
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use std::{
path::{Path, PathBuf},
@@ -31,7 +30,6 @@ use wit::Extension;
pub(crate) struct WasmHost {
engine: Engine,
release_channel: ReleaseChannel,
http_client: Arc<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
pub(crate) language_registry: Arc<LanguageRegistry>,
@@ -98,7 +96,6 @@ impl WasmHost {
http_client,
node_runtime,
language_registry,
release_channel: ReleaseChannel::global(cx),
_main_thread_message_task: task,
main_thread_message_tx: tx,
})
@@ -127,13 +124,8 @@ impl WasmHost {
},
);
let (mut extension, instance) = Extension::instantiate_async(
&mut store,
this.release_channel,
zed_api_version,
&component,
)
.await?;
let (mut extension, instance) =
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
extension
.call_init_extension(&mut store)

View File

@@ -1,9 +1,7 @@
mod since_v0_0_1;
mod since_v0_0_4;
mod since_v0_0_6;
mod since_v0_0_7;
use release_channel::ReleaseChannel;
use since_v0_0_7 as latest;
use since_v0_0_6 as latest;
use super::{wasm_engine, WasmState};
use anyhow::{Context, Result};
@@ -37,27 +35,17 @@ fn wasi_view(state: &mut WasmState) -> &mut WasmState {
}
/// Returns whether the given Wasm API version is supported by the Wasm host.
pub fn is_supported_wasm_api_version(
release_channel: ReleaseChannel,
version: SemanticVersion,
) -> bool {
wasm_api_version_range(release_channel).contains(&version)
pub fn is_supported_wasm_api_version(version: SemanticVersion) -> bool {
wasm_api_version_range().contains(&version)
}
/// Returns the Wasm API version range that is supported by the Wasm host.
#[inline(always)]
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
let max_version = if release_channel == ReleaseChannel::Dev {
latest::MAX_VERSION
} else {
since_v0_0_6::MAX_VERSION
};
since_v0_0_1::MIN_VERSION..=max_version
pub fn wasm_api_version_range() -> RangeInclusive<SemanticVersion> {
since_v0_0_1::MIN_VERSION..=latest::MAX_VERSION
}
pub enum Extension {
V007(since_v0_0_7::Extension),
V006(since_v0_0_6::Extension),
V004(since_v0_0_4::Extension),
V001(since_v0_0_1::Extension),
@@ -66,24 +54,14 @@ pub enum Extension {
impl Extension {
pub async fn instantiate_async(
store: &mut Store<WasmState>,
release_channel: ReleaseChannel,
version: SemanticVersion,
component: &Component,
) -> Result<(Self, Instance)> {
if release_channel == ReleaseChannel::Dev && version >= latest::MIN_VERSION {
if version >= latest::MIN_VERSION {
let (extension, instance) =
latest::Extension::instantiate_async(store, &component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V007(extension), instance))
} else if version >= since_v0_0_6::MIN_VERSION {
let (extension, instance) = since_v0_0_6::Extension::instantiate_async(
store,
&component,
since_v0_0_6::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V006(extension), instance))
} else if version >= since_v0_0_4::MIN_VERSION {
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
@@ -108,7 +86,6 @@ impl Extension {
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V007(ext) => ext.call_init_extension(store).await,
Extension::V006(ext) => ext.call_init_extension(store).await,
Extension::V004(ext) => ext.call_init_extension(store).await,
Extension::V001(ext) => ext.call_init_extension(store).await,
@@ -123,14 +100,10 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V007(ext) => {
Extension::V006(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V006(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V004(ext) => Ok(ext
.call_language_server_command(store, config, resource)
.await?
@@ -150,14 +123,6 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V007(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V006(ext) => {
ext.call_language_server_initialization_options(
store,
@@ -188,14 +153,6 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V007(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V006(ext) => {
ext.call_language_server_workspace_configuration(
store,
@@ -215,20 +172,11 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V007(ext) => {
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V006(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V006(ext) => Ok(ext
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
@@ -239,20 +187,11 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V007(ext) => {
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V006(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V006(ext) => Ok(ext
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
}

View File

@@ -1,10 +1,21 @@
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::settings::Settings;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use language::LspAdapterDelegate;
use futures::{io::BufReader, FutureExt as _};
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
@@ -15,13 +26,11 @@ wasmtime::component::bindgen!({
path: "../extension_api/wit/since_v0.0.6",
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
},
});
pub use self::zed::extension::*;
mod settings {
include!("../../../../extension_api/wit/since_v0.0.6/settings.rs");
}
@@ -30,93 +39,7 @@ pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| {
super::new_linker(|linker, f| {
Extension::add_to_linker(linker, f)?;
latest::zed::extension::github::add_to_linker(linker, f)?;
latest::zed::extension::nodejs::add_to_linker(linker, f)?;
latest::zed::extension::platform::add_to_linker(linker, f)?;
Ok(())
})
})
}
impl From<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
impl From<Range> for latest::Range {
fn from(value: Range) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<CodeLabel> for latest::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
#[async_trait]
@@ -125,14 +48,16 @@ impl HostWorktree for WasmState {
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
latest::HostWorktree::id(self, delegate).await
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
latest::HostWorktree::root_path(self, delegate).await
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
@@ -140,14 +65,19 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
@@ -155,7 +85,11 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
@@ -164,6 +98,107 @@ impl HostWorktree for WasmState {
}
}
#[async_trait]
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
#[async_trait]
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = http::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(github::GithubRelease {
version: release.tag_name,
assets: release
.assets
.into_iter()
.map(|asset| github::GithubReleaseAsset {
name: asset.name,
download_url: asset.browser_download_url,
})
.collect(),
})
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
@@ -172,13 +207,50 @@ impl ExtensionImports for WasmState {
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
latest::ExtensionImports::get_settings(
self,
location.map(|location| location.into()),
category,
key,
)
.await
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: location.worktree_id as usize,
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let settings =
AllLanguageSettings::get(location, cx).language(key.as_deref());
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&Arc::<str>::from(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
@@ -186,12 +258,23 @@ impl ExtensionImports for WasmState {
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
@@ -200,10 +283,103 @@ impl ExtensionImports for WasmState {
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg("-d")
.arg(&destination_path)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
latest::ExtensionImports::make_file_executable(self, path).await
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View File

@@ -1,408 +0,0 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::settings::Settings;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit/since_v0.0.7",
with: {
"worktree": ExtensionWorktree,
},
});
pub use self::zed::extension::*;
mod settings {
include!("../../../../extension_api/wit/since_v0.0.7/settings.rs");
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
}
#[async_trait]
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<http::github::GithubRelease> for github::GithubRelease {
fn from(value: http::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: http::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
#[async_trait]
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = http::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release =
http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: location.worktree_id as usize,
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let settings =
AllLanguageSettings::get(location, cx).language(key.as_deref());
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&Arc::<str>::from(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg("-d")
.arg(&destination_path)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_extension_api"
version = "0.0.7"
version = "0.0.6"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"

View File

@@ -16,8 +16,7 @@ pub use serde_json;
pub use wit::{
download_file, make_file_executable,
zed::extension::github::{
github_release_by_tag_name, latest_github_release, GithubRelease, GithubReleaseAsset,
GithubReleaseOptions,
latest_github_release, GithubRelease, GithubReleaseAsset, GithubReleaseOptions,
},
zed::extension::nodejs::{
node_binary_path, npm_install_package, npm_package_installed_version,
@@ -141,7 +140,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.0.7",
path: "./wit/since_v0.0.6",
});
}

View File

@@ -1,4 +1,4 @@
#[path = "../wit/since_v0.0.7/settings.rs"]
#[path = "../wit/since_v0.0.6/settings.rs"]
mod types;
use crate::{wit, Result, SettingsLocation, Worktree};

View File

@@ -97,7 +97,7 @@ world extension {
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the displayed label to include when filtering.
/// The range of the code to include when filtering.
filter-range: range,
}

View File

@@ -1,130 +0,0 @@
package zed:extension;
world extension {
import github;
import platform;
import nodejs;
use lsp.{completion, symbol};
/// Initializes the extension.
export init-extension: func();
/// The type of a downloaded file.
enum downloaded-file-type {
/// A gzipped file (`.gz`).
gzip,
/// A gzipped tar archive (`.tar.gz`).
gzip-tar,
/// A ZIP file (`.zip`).
zip,
/// An uncompressed file.
uncompressed,
}
/// The installation status for a language server.
variant language-server-installation-status {
/// The language server has no installation status.
none,
/// The language server is being downloaded.
downloading,
/// The language server is checking for updates.
checking-for-update,
/// The language server installation failed for specified reason.
failed(string),
}
record settings-location {
worktree-id: u64,
path: string,
}
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
/// Downloads a file from the given URL and saves it to the given path within the extension's
/// working directory.
///
/// The file will be extracted according to the given file type.
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
/// Makes the file at the given path executable.
import make-file-executable: func(filepath: string) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
/// A list of environment variables.
type env-vars = list<tuple<string, string>>;
/// A command.
record command {
/// The command to execute.
command: string,
/// The arguments to pass to the command.
args: list<string>,
/// The environment variables to set for the command.
env: env-vars,
}
/// A Zed worktree.
resource worktree {
/// Returns the ID of the worktree.
id: func() -> u64;
/// Returns the root path of the worktree.
root-path: func() -> string;
/// Returns the textual contents of the specified file in the worktree.
read-text-file: func(path: string) -> result<string, string>;
/// Returns the path to the given binary name, if one is present on the `$PATH`.
which: func(binary-name: string) -> option<string>;
/// Returns the current shell environment.
shell-env: func() -> env-vars;
}
/// Returns the command used to start up the language server.
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
/// Returns the initialization options to pass to the language server on startup.
///
/// The initialization options are represented as a JSON string.
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the language server.
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// A label containing some code.
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the displayed label to include when filtering.
filter-range: range,
}
/// A span within a code label.
variant code-label-span {
/// A range into the parsed code.
code-range(range),
/// A span containing a code literal.
literal(code-label-span-literal),
}
/// A span containing a code literal.
record code-label-span-literal {
/// The literal text.
text: string,
/// The name of the highlight to use for this literal.
highlight-name: option<string>,
}
/// A (half-open) range (`[start, end)`).
record range {
/// The start of the range (inclusive).
start: u32,
/// The end of the range (exclusive).
end: u32,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
}

View File

@@ -1,33 +0,0 @@
interface github {
/// A GitHub release.
record github-release {
/// The version of the release.
version: string,
/// The list of assets attached to the release.
assets: list<github-release-asset>,
}
/// An asset from a GitHub release.
record github-release-asset {
/// The name of the asset.
name: string,
/// The download URL for the asset.
download-url: string,
}
/// The options used to filter down GitHub releases.
record github-release-options {
/// Whether releases without assets should be included.
require-assets: bool,
/// Whether pre-releases should be included.
pre-release: bool,
}
/// Returns the latest release for the given GitHub repository.
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
///
/// Returns an error if a release with the given tag name does not exist.
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
}

View File

@@ -1,83 +0,0 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
/// The kind of an LSP completion.
variant completion-kind {
text,
method,
function,
%constructor,
field,
variable,
class,
%interface,
module,
property,
unit,
value,
%enum,
keyword,
snippet,
color,
file,
reference,
folder,
enum-member,
constant,
struct,
event,
operator,
type-parameter,
other(s32),
}
/// Defines how to interpret the insert text in a completion item.
variant insert-text-format {
plain-text,
snippet,
other(s32),
}
/// An LSP symbol.
record symbol {
kind: symbol-kind,
name: string,
}
/// The kind of an LSP symbol.
variant symbol-kind {
file,
module,
namespace,
%package,
class,
method,
property,
field,
%constructor,
%enum,
%interface,
function,
variable,
constant,
%string,
number,
boolean,
array,
object,
key,
null,
enum-member,
struct,
event,
operator,
type-parameter,
other(s32),
}
}

View File

@@ -1,13 +0,0 @@
interface nodejs {
/// Returns the path to the Node binary used by Zed.
node-binary-path: func() -> result<string, string>;
/// Returns the latest version of the given NPM package.
npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
/// Installs the specified NPM package.
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
}

Some files were not shown because too many files have changed in this diff Show More