mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Merge branch 'main' into pixel-columns
This commit is contained in:
commit
9d07561d99
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@ -2,11 +2,4 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
- N/A
|
||||
|
||||
or
|
||||
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
19
.github/workflows/release_actions.yml
vendored
19
.github/workflows/release_actions.yml
vendored
@ -6,8 +6,8 @@ jobs:
|
||||
discord_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get appropriate URL
|
||||
id: get-appropriate-url
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
@ -15,14 +15,17 @@ jobs:
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.2.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
|
||||
|
||||
${{ github.event.release.body }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
125
Cargo.lock
generated
125
Cargo.lock
generated
@ -103,7 +103,7 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tiktoken-rs 0.5.4",
|
||||
"tiktoken-rs",
|
||||
"util",
|
||||
]
|
||||
|
||||
@ -316,12 +316,13 @@ dependencies = [
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
"semantic_index",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"tiktoken-rs 0.4.5",
|
||||
"tiktoken-rs",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"workspace",
|
||||
@ -1466,7 +1467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.23.3"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1501,6 +1502,7 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@ -1623,6 +1625,7 @@ dependencies = [
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed-actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2403,7 +2406,6 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
@ -3988,6 +3990,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rpc",
|
||||
@ -5517,6 +5520,26 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "prettier"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
@ -5629,8 +5652,10 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"prettier",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@ -6600,12 +6625,6 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
|
||||
[[package]]
|
||||
name = "rustybuzz"
|
||||
version = "0.3.0"
|
||||
@ -6925,7 +6944,7 @@ dependencies = [
|
||||
"smol",
|
||||
"tempdir",
|
||||
"theme",
|
||||
"tiktoken-rs 0.5.4",
|
||||
"tiktoken-rs",
|
||||
"tree-sitter",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-elixir",
|
||||
@ -6939,7 +6958,6 @@ dependencies = [
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7659,28 +7677,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "storybook"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap 4.4.4",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"itertools 0.11.0",
|
||||
"log",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"settings",
|
||||
"simplelog",
|
||||
"strum",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.4"
|
||||
@ -7703,22 +7699,6 @@ name = "strum"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
@ -8139,21 +8119,6 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.4",
|
||||
"bstr",
|
||||
"fancy-regex",
|
||||
"lazy_static",
|
||||
"parking_lot 0.12.1",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.5.4"
|
||||
@ -8814,6 +8779,15 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
@ -8885,21 +8859,6 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"gpui2",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
@ -9986,6 +9945,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project",
|
||||
@ -10081,9 +10041,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.108.0"
|
||||
version = "0.110.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"async-compression",
|
||||
@ -10196,6 +10157,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"url",
|
||||
@ -10213,6 +10175,7 @@ name = "zed-actions"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -52,6 +52,7 @@ members = [
|
||||
"crates/plugin",
|
||||
"crates/plugin_macros",
|
||||
"crates/plugin_runtime",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
@ -65,13 +66,11 @@ members = [
|
||||
"crates/sqlez_macros",
|
||||
"crates/feature_flags",
|
||||
"crates/rich_text",
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme_selector",
|
||||
"crates/ui",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/vim",
|
||||
@ -150,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
2
Procfile
2
Procfile
@ -1,4 +1,4 @@
|
||||
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||
livekit: livekit-server --dev
|
||||
postgrest: postgrest crates/collab/admin_api.conf
|
||||
|
3
assets/icons/link.svg
Normal file
3
assets/icons/link.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.51192 3.00541C9.18827 2.54594 10.0434 2.53694 10.6788 2.95419C10.823 3.04893 10.9771 3.1993 11.389 3.61119C11.8009 4.02307 11.9513 4.17714 12.046 4.32141C12.4632 4.95675 12.4542 5.81192 11.9948 6.48827C11.8899 6.64264 11.7276 6.80811 11.3006 7.23511L10.6819 7.85383C10.4866 8.04909 10.4866 8.36567 10.6819 8.56093C10.8772 8.7562 11.1937 8.7562 11.389 8.56093L12.0077 7.94221L12.0507 7.89929C12.4203 7.52976 12.6568 7.2933 12.822 7.0502C13.4972 6.05623 13.5321 4.76252 12.8819 3.77248C12.7233 3.53102 12.4922 3.30001 12.1408 2.94871L12.0961 2.90408L12.0515 2.85942C11.7002 2.508 11.4692 2.27689 11.2277 2.11832C10.2377 1.46813 8.94396 1.50299 7.94999 2.17822C7.70689 2.34336 7.47042 2.57991 7.10088 2.94955L7.05797 2.99247L6.43926 3.61119C6.24399 3.80645 6.24399 4.12303 6.43926 4.31829C6.63452 4.51355 6.9511 4.51355 7.14636 4.31829L7.76508 3.69957C8.19208 3.27257 8.35755 3.11027 8.51192 3.00541ZM4.31794 7.14672C4.5132 6.95146 4.5132 6.63487 4.31794 6.43961C4.12267 6.24435 3.80609 6.24435 3.61083 6.43961L2.99211 7.05833L2.9492 7.10124C2.57955 7.47077 2.34301 7.70724 2.17786 7.95035C1.50263 8.94432 1.46778 10.238 2.11797 11.2281C2.27654 11.4695 2.50764 11.7005 2.85908 12.0518L2.90372 12.0965L2.94835 12.1411C3.29965 12.4925 3.53066 12.7237 3.77212 12.8822C4.76217 13.5324 6.05587 13.4976 7.04984 12.8223C7.29294 12.6572 7.52941 12.4206 7.89894 12.051L7.89895 12.051L7.94186 12.0081L8.56058 11.3894C8.75584 11.1941 8.75584 10.8775 8.56058 10.6823C8.36531 10.487 8.04873 10.487 7.85347 10.6823L7.23475 11.301C6.80775 11.728 6.64228 11.8903 6.48792 11.9951C5.81156 12.4546 4.9564 12.4636 4.32105 12.0464C4.17679 11.9516 4.02272 11.8012 3.61083 11.3894C3.19894 10.9775 3.04858 10.8234 2.95383 10.6791C2.53659 10.0438 2.54558 9.18863 3.00505 8.51227C3.10991 8.35791 3.27222 8.19244 3.69922 7.76544L4.31794 7.14672ZM9.6217 6.08558C9.81696 5.89032 9.81696 5.57373 9.6217 5.37847C9.42644 5.18321 9.10986 5.18321 8.91459 5.37847L5.37906 8.91401C5.1838 9.10927 5.1838 9.42585 5.37906 9.62111C5.57432 9.81637 5.8909 9.81637 6.08617 9.62111L9.6217 6.08558Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
3
assets/icons/public.svg
Normal file
3
assets/icons/public.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74393 2.00204C3.41963 1.97524 3.13502 2.37572 3.10823 2.70001C3.08143 3.0243 3.32558 3.47321 3.64986 3.50001C7.99878 3.85934 11.1406 7.00122 11.5 11.3501C11.5267 11.6744 11.9756 12.0269 12.3 12C12.6243 11.9733 13.0247 11.5804 12.998 11.2561C12.5912 6.33295 8.66704 2.40882 3.74393 2.00204ZM2.9 6.00001C2.96411 5.68099 3.33084 5.29361 3.64986 5.35772C6.66377 5.96341 9.03654 8.33618 9.64223 11.3501C9.70634 11.6691 9.319 12.0359 8.99999 12.1C8.68097 12.1641 8.06411 11.819 7.99999 11.5C7.48788 8.95167 6.0483 7.51213 3.49999 7.00001C3.18097 6.9359 2.8359 6.31902 2.9 6.00001ZM2 9.20001C2.0641 8.88099 2.38635 8.65788 2.70537 8.722C4.50255 9.08317 5.91684 10.4975 6.27801 12.2946C6.34212 12.6137 6.13547 12.9242 5.81646 12.9883C5.49744 13.0525 4.86411 12.819 4.8 12.5C4.53239 11.1683 3.83158 10.4676 2.5 10.2C2.18098 10.1359 1.93588 9.51902 2 9.20001Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1021 B |
8
assets/icons/update.svg
Normal file
8
assets/icons/update.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1.90321 7.29677C1.90321 10.341 4.11041 12.4147 6.58893 12.8439C6.87255 12.893 7.06266 13.1627 7.01355 13.4464C6.96444 13.73 6.69471 13.9201 6.41109 13.871C3.49942 13.3668 0.86084 10.9127 0.86084 7.29677C0.860839 5.76009 1.55996 4.55245 2.37639 3.63377C2.96124 2.97568 3.63034 2.44135 4.16846 2.03202L2.53205 2.03202C2.25591 2.03202 2.03205 1.80816 2.03205 1.53202C2.03205 1.25588 2.25591 1.03202 2.53205 1.03202L5.53205 1.03202C5.80819 1.03202 6.03205 1.25588 6.03205 1.53202L6.03205 4.53202C6.03205 4.80816 5.80819 5.03202 5.53205 5.03202C5.25591 5.03202 5.03205 4.80816 5.03205 4.53202L5.03205 2.68645L5.03054 2.68759L5.03045 2.68766L5.03044 2.68767L5.03043 2.68767C4.45896 3.11868 3.76059 3.64538 3.15554 4.3262C2.44102 5.13021 1.90321 6.10154 1.90321 7.29677ZM13.0109 7.70321C13.0109 4.69115 10.8505 2.6296 8.40384 2.17029C8.12093 2.11718 7.93465 1.84479 7.98776 1.56188C8.04087 1.27898 8.31326 1.0927 8.59616 1.14581C11.4704 1.68541 14.0532 4.12605 14.0532 7.70321C14.0532 9.23988 13.3541 10.4475 12.5377 11.3662C11.9528 12.0243 11.2837 12.5586 10.7456 12.968L12.3821 12.968C12.6582 12.968 12.8821 13.1918 12.8821 13.468C12.8821 13.7441 12.6582 13.968 12.3821 13.968L9.38205 13.968C9.10591 13.968 8.88205 13.7441 8.88205 13.468L8.88205 10.468C8.88205 10.1918 9.10591 9.96796 9.38205 9.96796C9.65819 9.96796 9.88205 10.1918 9.88205 10.468L9.88205 12.3135L9.88362 12.3123C10.4551 11.8813 11.1535 11.3546 11.7585 10.6738C12.4731 9.86976 13.0109 8.89844 13.0109 7.70321Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -408,6 +408,7 @@
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
|
@ -50,6 +50,9 @@
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether to display inline and alongside documentation for items in the
|
||||
// completions menu
|
||||
"show_completion_documentation": true,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if softwrap is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
@ -76,7 +79,7 @@
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone muted by default
|
||||
"mute_on_join": true
|
||||
"mute_on_join": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@ -199,7 +202,12 @@
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// 3. Format code using Zed's Prettier integration:
|
||||
// "formatter": "prettier"
|
||||
// 4. Default. Format files using Zed's Prettier integration (if applicable),
|
||||
// or falling back to formatting via language server:
|
||||
// "formatter": "auto"
|
||||
"formatter": "auto",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
@ -429,6 +437,16 @@
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
// project has no other Prettier installed.
|
||||
"prettier": {
|
||||
// Use regular Prettier json configuration:
|
||||
// "trailingComma": "es5",
|
||||
// "tabWidth": 4,
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
@ -85,25 +85,6 @@ impl Embedding {
|
||||
}
|
||||
}
|
||||
|
||||
// impl FromSql for Embedding {
|
||||
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
// let bytes = value.as_blob()?;
|
||||
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
||||
// if embedding.is_err() {
|
||||
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
||||
// }
|
||||
// Ok(Embedding(embedding.unwrap()))
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl ToSql for Embedding {
|
||||
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
// let bytes = bincode::serialize(&self.0)
|
||||
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAIEmbeddings {
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
|
||||
request_timeout,
|
||||
)
|
||||
.await?;
|
||||
|
||||
request_number += 1;
|
||||
|
||||
match response.status() {
|
||||
|
@ -22,8 +22,11 @@ settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
uuid.workspace = true
|
||||
semantic_index = { path = "../semantic_index" }
|
||||
project = { path = "../project" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
@ -36,7 +39,7 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs = "0.4"
|
||||
tiktoken-rs = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
prompts::generate_content_prompt,
|
||||
prompts::{generate_content_prompt, PromptCodeSnippet},
|
||||
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
|
||||
SavedMessage,
|
||||
};
|
||||
@ -17,7 +17,7 @@ use editor::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||
},
|
||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
@ -29,13 +29,15 @@ use gpui::{
|
||||
},
|
||||
fonts::HighlightStyle,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
|
||||
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
WeakModelHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
|
||||
use project::Project;
|
||||
use search::BufferSearchBar;
|
||||
use semantic_index::{SemanticIndex, SemanticIndexStatus};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
@ -46,7 +48,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::{
|
||||
components::{action_button::Button, ComponentExt},
|
||||
@ -72,6 +74,7 @@ actions!(
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
]
|
||||
);
|
||||
|
||||
@ -108,6 +111,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(InlineAssistant::confirm);
|
||||
cx.add_action(InlineAssistant::cancel);
|
||||
cx.add_action(InlineAssistant::toggle_include_conversation);
|
||||
cx.add_action(InlineAssistant::toggle_retrieve_context);
|
||||
cx.add_action(InlineAssistant::move_up);
|
||||
cx.add_action(InlineAssistant::move_down);
|
||||
}
|
||||
@ -145,6 +149,8 @@ pub struct AssistantPanel {
|
||||
include_conversation_in_next_inline_assist: bool,
|
||||
inline_prompt_history: VecDeque<String>,
|
||||
_watch_saved_conversations: Task<Result<()>>,
|
||||
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||
retrieve_context_in_next_inline_assist: bool,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
@ -191,6 +197,9 @@ impl AssistantPanel {
|
||||
toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
|
||||
toolbar
|
||||
});
|
||||
|
||||
let semantic_index = SemanticIndex::global(cx);
|
||||
|
||||
let mut this = Self {
|
||||
workspace: workspace_handle,
|
||||
active_editor_index: Default::default(),
|
||||
@ -215,6 +224,8 @@ impl AssistantPanel {
|
||||
include_conversation_in_next_inline_assist: false,
|
||||
inline_prompt_history: Default::default(),
|
||||
_watch_saved_conversations,
|
||||
semantic_index,
|
||||
retrieve_context_in_next_inline_assist: false,
|
||||
};
|
||||
|
||||
let mut old_dock_position = this.position(cx);
|
||||
@ -262,12 +273,19 @@ impl AssistantPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
|
||||
this.update(cx, |assistant, cx| {
|
||||
assistant.new_inline_assist(&active_editor, cx)
|
||||
assistant.new_inline_assist(&active_editor, cx, project)
|
||||
});
|
||||
}
|
||||
|
||||
fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
fn new_inline_assist(
|
||||
&mut self,
|
||||
editor: &ViewHandle<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
project: &ModelHandle<Project>,
|
||||
) {
|
||||
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
|
||||
api_key
|
||||
} else {
|
||||
@ -278,26 +296,61 @@ impl AssistantPanel {
|
||||
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
||||
return;
|
||||
}
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
||||
// Extend the selection to the start and the end of the line.
|
||||
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
|
||||
if point_selection.end > point_selection.start {
|
||||
point_selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if point_selection.end.column == 0 {
|
||||
point_selection.end.row -= 1;
|
||||
}
|
||||
point_selection.end.column = snapshot.line_len(point_selection.end.row);
|
||||
}
|
||||
|
||||
let codegen_kind = if point_selection.start == point_selection.end {
|
||||
CodegenKind::Generate {
|
||||
position: snapshot.anchor_after(point_selection.start),
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: snapshot.anchor_before(point_selection.start)
|
||||
..snapshot.anchor_after(point_selection.end),
|
||||
}
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
api_key,
|
||||
cx.background().clone(),
|
||||
));
|
||||
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
||||
CodegenKind::Generate {
|
||||
position: selection.start,
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: selection.start..selection.end,
|
||||
}
|
||||
};
|
||||
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
});
|
||||
|
||||
if let Some(semantic_index) = self.semantic_index.clone() {
|
||||
let project = project.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let previously_indexed = semantic_index
|
||||
.update(&mut cx, |index, cx| {
|
||||
index.project_previously_indexed(&project, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if previously_indexed {
|
||||
let _ = semantic_index
|
||||
.update(&mut cx, |index, cx| {
|
||||
index.index_project(project.clone(), cx)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
|
||||
let inline_assistant = cx.add_view(|cx| {
|
||||
let assistant = InlineAssistant::new(
|
||||
@ -308,6 +361,9 @@ impl AssistantPanel {
|
||||
codegen.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
self.retrieve_context_in_next_inline_assist,
|
||||
self.semantic_index.clone(),
|
||||
project.clone(),
|
||||
);
|
||||
cx.focus_self();
|
||||
assistant
|
||||
@ -319,7 +375,7 @@ impl AssistantPanel {
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
position: selection.head().bias_left(&snapshot),
|
||||
position: snapshot.anchor_before(point_selection.head()),
|
||||
height: 2,
|
||||
render: Arc::new({
|
||||
let inline_assistant = inline_assistant.clone();
|
||||
@ -348,6 +404,7 @@ impl AssistantPanel {
|
||||
editor: editor.downgrade(),
|
||||
inline_assistant: Some((block_id, inline_assistant.clone())),
|
||||
codegen: codegen.clone(),
|
||||
project: project.downgrade(),
|
||||
_subscriptions: vec![
|
||||
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
|
||||
cx.subscribe(editor, {
|
||||
@ -426,8 +483,15 @@ impl AssistantPanel {
|
||||
InlineAssistantEvent::Confirmed {
|
||||
prompt,
|
||||
include_conversation,
|
||||
retrieve_context,
|
||||
} => {
|
||||
self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
|
||||
self.confirm_inline_assist(
|
||||
assist_id,
|
||||
prompt,
|
||||
*include_conversation,
|
||||
cx,
|
||||
*retrieve_context,
|
||||
);
|
||||
}
|
||||
InlineAssistantEvent::Canceled => {
|
||||
self.finish_inline_assist(assist_id, true, cx);
|
||||
@ -440,6 +504,9 @@ impl AssistantPanel {
|
||||
} => {
|
||||
self.include_conversation_in_next_inline_assist = *include_conversation;
|
||||
}
|
||||
InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
|
||||
self.retrieve_context_in_next_inline_assist = *retrieve_context
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,6 +585,7 @@ impl AssistantPanel {
|
||||
user_prompt: &str,
|
||||
include_conversation: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
retrieve_context: bool,
|
||||
) {
|
||||
let conversation = if include_conversation {
|
||||
self.active_editor()
|
||||
@ -539,6 +607,8 @@ impl AssistantPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = pending_assist.project.clone();
|
||||
|
||||
self.inline_prompt_history
|
||||
.retain(|prompt| prompt != user_prompt);
|
||||
self.inline_prompt_history.push_back(user_prompt.into());
|
||||
@ -578,14 +648,63 @@ impl AssistantPanel {
|
||||
|
||||
let codegen_kind = codegen.read(cx).kind().clone();
|
||||
let user_prompt = user_prompt.to_string();
|
||||
let prompt = cx.background().spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
||||
});
|
||||
let mut messages = Vec::new();
|
||||
|
||||
let snippets = if retrieve_context {
|
||||
let Some(project) = project.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
|
||||
let search_results = semantic_index.update(cx, |this, cx| {
|
||||
this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
|
||||
});
|
||||
|
||||
cx.background()
|
||||
.spawn(async move { search_results.await.unwrap_or_default() })
|
||||
} else {
|
||||
Task::ready(Vec::new())
|
||||
};
|
||||
|
||||
let snippets = cx.spawn(|_, cx| async move {
|
||||
let mut snippets = Vec::new();
|
||||
for result in search_results.await {
|
||||
snippets.push(PromptCodeSnippet::new(result, &cx));
|
||||
|
||||
// snippets.push(result.buffer.read_with(&cx, |buffer, _| {
|
||||
// buffer
|
||||
// .snapshot()
|
||||
// .text_for_range(result.range)
|
||||
// .collect::<String>()
|
||||
// }));
|
||||
}
|
||||
snippets
|
||||
});
|
||||
snippets
|
||||
} else {
|
||||
Task::ready(Vec::new())
|
||||
};
|
||||
|
||||
let mut model = settings::get::<AssistantSettings>(cx)
|
||||
.default_open_ai_model
|
||||
.clone();
|
||||
let model_name = model.full_name();
|
||||
|
||||
let prompt = cx.background().spawn(async move {
|
||||
let snippets = snippets.await;
|
||||
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(
|
||||
user_prompt,
|
||||
language_name,
|
||||
&buffer,
|
||||
range,
|
||||
codegen_kind,
|
||||
snippets,
|
||||
model_name,
|
||||
)
|
||||
});
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if let Some(conversation) = conversation {
|
||||
let conversation = conversation.read(cx);
|
||||
let buffer = conversation.buffer.read(cx);
|
||||
@ -1498,12 +1617,14 @@ impl Conversation {
|
||||
Role::Assistant => "assistant".into(),
|
||||
Role::System => "system".into(),
|
||||
},
|
||||
content: self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.text_for_range(message.offset_range)
|
||||
.collect(),
|
||||
content: Some(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.text_for_range(message.offset_range)
|
||||
.collect(),
|
||||
),
|
||||
name: None,
|
||||
function_call: None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@ -2622,12 +2743,16 @@ enum InlineAssistantEvent {
|
||||
Confirmed {
|
||||
prompt: String,
|
||||
include_conversation: bool,
|
||||
retrieve_context: bool,
|
||||
},
|
||||
Canceled,
|
||||
Dismissed,
|
||||
IncludeConversationToggled {
|
||||
include_conversation: bool,
|
||||
},
|
||||
RetrieveContextToggled {
|
||||
retrieve_context: bool,
|
||||
},
|
||||
}
|
||||
|
||||
struct InlineAssistant {
|
||||
@ -2643,6 +2768,11 @@ struct InlineAssistant {
|
||||
pending_prompt: String,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
retrieve_context: bool,
|
||||
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||
semantic_permissioned: Option<bool>,
|
||||
project: WeakModelHandle<Project>,
|
||||
maintain_rate_limit: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl Entity for InlineAssistant {
|
||||
@ -2659,51 +2789,65 @@ impl View for InlineAssistant {
|
||||
let theme = theme::current(cx);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Button::action(ToggleIncludeConversation)
|
||||
.with_tooltip("Include Conversation", theme.tooltip.clone())
|
||||
.with_children([Flex::row()
|
||||
.with_child(
|
||||
Button::action(ToggleIncludeConversation)
|
||||
.with_tooltip("Include Conversation", theme.tooltip.clone())
|
||||
.with_id(self.id)
|
||||
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
|
||||
.toggleable(self.include_conversation)
|
||||
.with_style(theme.assistant.inline.include_conversation.clone())
|
||||
.element()
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(if SemanticIndex::enabled(cx) {
|
||||
Some(
|
||||
Button::action(ToggleRetrieveContext)
|
||||
.with_tooltip("Retrieve Context", theme.tooltip.clone())
|
||||
.with_id(self.id)
|
||||
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
|
||||
.toggleable(self.include_conversation)
|
||||
.with_style(theme.assistant.inline.include_conversation.clone())
|
||||
.with_contents(theme::components::svg::Svg::new(
|
||||
"icons/magnifying_glass.svg",
|
||||
))
|
||||
.toggleable(self.retrieve_context)
|
||||
.with_style(theme.assistant.inline.retrieve_context.clone())
|
||||
.element()
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
||||
Some(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(theme.assistant.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.error_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.error_icon.container)
|
||||
.with_tooltip::<ErrorIcon>(
|
||||
self.id,
|
||||
error.to_string(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
.dynamically({
|
||||
let measurements = self.measurements.clone();
|
||||
move |constraint, _, _| {
|
||||
let measurements = measurements.get();
|
||||
SizeConstraint {
|
||||
min: vec2f(measurements.gutter_width, constraint.min.y()),
|
||||
max: vec2f(measurements.gutter_width, constraint.max.y()),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
||||
Some(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(theme.assistant.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.error_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.error_icon.container)
|
||||
.with_tooltip::<ErrorIcon>(
|
||||
self.id,
|
||||
error.to_string(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
.dynamically({
|
||||
let measurements = self.measurements.clone();
|
||||
move |constraint, _, _| {
|
||||
let measurements = measurements.get();
|
||||
SizeConstraint {
|
||||
min: vec2f(measurements.gutter_width, constraint.min.y()),
|
||||
max: vec2f(measurements.gutter_width, constraint.max.y()),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
})])
|
||||
.with_child(Empty::new().constrained().dynamically({
|
||||
let measurements = self.measurements.clone();
|
||||
move |constraint, _, _| {
|
||||
@ -2726,6 +2870,16 @@ impl View for InlineAssistant {
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(if self.retrieve_context {
|
||||
Some(
|
||||
Flex::row()
|
||||
.with_children(self.retrieve_context_status(cx))
|
||||
.flex(1., true)
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.assistant.inline.container)
|
||||
.into_any()
|
||||
@ -2751,6 +2905,9 @@ impl InlineAssistant {
|
||||
codegen: ModelHandle<Codegen>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
retrieve_context: bool,
|
||||
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||
project: ModelHandle<Project>,
|
||||
) -> Self {
|
||||
let prompt_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
@ -2764,11 +2921,16 @@ impl InlineAssistant {
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor
|
||||
});
|
||||
let subscriptions = vec![
|
||||
let mut subscriptions = vec![
|
||||
cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
|
||||
];
|
||||
Self {
|
||||
|
||||
if let Some(semantic_index) = semantic_index.clone() {
|
||||
subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
|
||||
}
|
||||
|
||||
let assistant = Self {
|
||||
id,
|
||||
prompt_editor,
|
||||
workspace,
|
||||
@ -2781,7 +2943,33 @@ impl InlineAssistant {
|
||||
pending_prompt: String::new(),
|
||||
codegen,
|
||||
_subscriptions: subscriptions,
|
||||
retrieve_context,
|
||||
semantic_permissioned: None,
|
||||
semantic_index,
|
||||
project: project.downgrade(),
|
||||
maintain_rate_limit: None,
|
||||
};
|
||||
|
||||
assistant.index_project(cx).log_err();
|
||||
|
||||
assistant
|
||||
}
|
||||
|
||||
fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
|
||||
if let Some(value) = self.semantic_permissioned {
|
||||
return Task::ready(Ok(value));
|
||||
}
|
||||
|
||||
let Some(project) = self.project.upgrade(cx) else {
|
||||
return Task::ready(Err(anyhow!("project was dropped")));
|
||||
};
|
||||
|
||||
self.semantic_index
|
||||
.as_ref()
|
||||
.map(|semantic| {
|
||||
semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
|
||||
})
|
||||
.unwrap_or(Task::ready(Ok(false)))
|
||||
}
|
||||
|
||||
fn handle_prompt_editor_events(
|
||||
@ -2796,6 +2984,37 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic_index_changed(
|
||||
&mut self,
|
||||
semantic_index: ModelHandle<SemanticIndex>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(project) = self.project.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let status = semantic_index.read(cx).status(&project);
|
||||
match status {
|
||||
SemanticIndexStatus::Indexing {
|
||||
rate_limit_expiry: Some(_),
|
||||
..
|
||||
} => {
|
||||
if self.maintain_rate_limit.is_none() {
|
||||
self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
|
||||
loop {
|
||||
cx.background().timer(Duration::from_secs(1)).await;
|
||||
this.update(&mut cx, |_, cx| cx.notify()).log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
self.maintain_rate_limit = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
|
||||
let is_read_only = !self.codegen.read(cx).idle();
|
||||
self.prompt_editor.update(cx, |editor, cx| {
|
||||
@ -2845,12 +3064,241 @@ impl InlineAssistant {
|
||||
cx.emit(InlineAssistantEvent::Confirmed {
|
||||
prompt,
|
||||
include_conversation: self.include_conversation,
|
||||
retrieve_context: self.retrieve_context,
|
||||
});
|
||||
self.confirmed = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
|
||||
let semantic_permissioned = self.semantic_permissioned(cx);
|
||||
|
||||
let Some(project) = self.project.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project_name = project
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/");
|
||||
let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0;
|
||||
let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name,
|
||||
if is_plural {
|
||||
"s"
|
||||
} else {""});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// If Necessary prompt user
|
||||
if !semantic_permissioned.await.unwrap_or(false) {
|
||||
let mut answer = this.update(&mut cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
prompt_text.as_str(),
|
||||
&["Continue", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
|
||||
if answer.next().await == Some(0) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.semantic_permissioned = Some(true);
|
||||
})?;
|
||||
} else {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If permissioned, update context appropriately
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.retrieve_context = !this.retrieve_context;
|
||||
|
||||
cx.emit(InlineAssistantEvent::RetrieveContextToggled {
|
||||
retrieve_context: this.retrieve_context,
|
||||
});
|
||||
|
||||
if this.retrieve_context {
|
||||
this.index_project(cx).log_err();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
|
||||
let Some(project) = self.project.upgrade(cx) else {
|
||||
return Err(anyhow!("project was dropped!"));
|
||||
};
|
||||
|
||||
let semantic_permissioned = self.semantic_permissioned(cx);
|
||||
if let Some(semantic_index) = SemanticIndex::global(cx) {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
// This has to be updated to accomodate for semantic_permissions
|
||||
if semantic_permissioned.await.unwrap_or(false) {
|
||||
semantic_index
|
||||
.update(&mut cx, |index, cx| index.index_project(project, cx))
|
||||
.await
|
||||
} else {
|
||||
Err(anyhow!("project is not permissioned for semantic indexing"))
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn retrieve_context_status(
|
||||
&self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<InlineAssistant>> {
|
||||
enum ContextStatusIcon {}
|
||||
|
||||
let Some(project) = self.project.upgrade(cx) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(semantic_index) = SemanticIndex::global(cx) {
|
||||
let status = semantic_index.update(cx, |index, _| index.status(&project));
|
||||
let theme = theme::current(cx);
|
||||
match status {
|
||||
SemanticIndexStatus::NotAuthenticated {} => Some(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(theme.assistant.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.error_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.error_icon.container)
|
||||
.with_tooltip::<ContextStatusIcon>(
|
||||
self.id,
|
||||
"Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any(),
|
||||
),
|
||||
SemanticIndexStatus::NotIndexed {} => Some(
|
||||
Svg::new("icons/error.svg")
|
||||
.with_color(theme.assistant.inline.context_status.error_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.inline.context_status.error_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.inline.context_status.error_icon.container)
|
||||
.with_tooltip::<ContextStatusIcon>(
|
||||
self.id,
|
||||
"Not Indexed",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any(),
|
||||
),
|
||||
SemanticIndexStatus::Indexing {
|
||||
remaining_files,
|
||||
rate_limit_expiry,
|
||||
} => {
|
||||
|
||||
let mut status_text = if remaining_files == 0 {
|
||||
"Indexing...".to_string()
|
||||
} else {
|
||||
format!("Remaining files to index: {remaining_files}")
|
||||
};
|
||||
|
||||
if let Some(rate_limit_expiry) = rate_limit_expiry {
|
||||
let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
|
||||
if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
|
||||
write!(
|
||||
status_text,
|
||||
" (rate limit expires in {}s)",
|
||||
remaining_seconds.as_secs()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Some(
|
||||
Svg::new("icons/update.svg")
|
||||
.with_color(theme.assistant.inline.context_status.in_progress_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.inline.context_status.in_progress_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.inline.context_status.in_progress_icon.container)
|
||||
.with_tooltip::<ContextStatusIcon>(
|
||||
self.id,
|
||||
status_text,
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
SemanticIndexStatus::Indexed {} => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.assistant.inline.context_status.complete_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.assistant.inline.context_status.complete_icon.width)
|
||||
.contained()
|
||||
.with_style(theme.assistant.inline.context_status.complete_icon.container)
|
||||
.with_tooltip::<ContextStatusIcon>(
|
||||
self.id,
|
||||
"Index up to date",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
|
||||
// let project = self.project.clone();
|
||||
// if let Some(semantic_index) = self.semantic_index.clone() {
|
||||
// let status = semantic_index.update(cx, |index, cx| index.status(&project));
|
||||
// return match status {
|
||||
// // This theoretically shouldnt be a valid code path
|
||||
// // As the inline assistant cant be launched without an API key
|
||||
// // We keep it here for safety
|
||||
// semantic_index::SemanticIndexStatus::NotAuthenticated => {
|
||||
// "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
|
||||
// }
|
||||
// semantic_index::SemanticIndexStatus::Indexed => {
|
||||
// "Indexing Complete!".to_string()
|
||||
// }
|
||||
// semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
|
||||
|
||||
// let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
|
||||
|
||||
// if let Some(rate_limit_expiry) = rate_limit_expiry {
|
||||
// let remaining_seconds =
|
||||
// rate_limit_expiry.duration_since(Instant::now());
|
||||
// if remaining_seconds > Duration::from_secs(0) {
|
||||
// write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
|
||||
// }
|
||||
// }
|
||||
// status
|
||||
// }
|
||||
// semantic_index::SemanticIndexStatus::NotIndexed => {
|
||||
// "Not Indexed for Context Retrieval".to_string()
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// "".to_string()
|
||||
// }
|
||||
|
||||
fn toggle_include_conversation(
|
||||
&mut self,
|
||||
_: &ToggleIncludeConversation,
|
||||
@ -2913,6 +3361,7 @@ struct PendingInlineAssist {
|
||||
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
|
||||
codegen: ModelHandle<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
project: WeakModelHandle<Project>,
|
||||
}
|
||||
|
||||
fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
|
@ -1,9 +1,7 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, OpenAIRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{
|
||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
@ -40,26 +38,11 @@ impl Entity for Codegen {
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
mut kind: CodegenKind,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
match &mut kind {
|
||||
CodegenKind::Transform { range } => {
|
||||
let mut point_range = range.to_point(&snapshot);
|
||||
point_range.start.column = 0;
|
||||
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
|
||||
point_range.end.column = snapshot.line_len(point_range.end.row);
|
||||
}
|
||||
range.start = snapshot.anchor_before(point_range.start);
|
||||
range.end = snapshot.anchor_after(point_range.end);
|
||||
}
|
||||
CodegenKind::Generate { position } => {
|
||||
*position = position.bias_right(&snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
@ -386,7 +369,7 @@ mod tests {
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
|
@ -1,9 +1,62 @@
|
||||
use crate::codegen::CodegenKind;
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use semantic_index::SearchResult;
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use tiktoken_rs::ChatCompletionRequestMessage;
|
||||
|
||||
pub struct PromptCodeSnippet {
|
||||
path: Option<PathBuf>,
|
||||
language_name: Option<String>,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl PromptCodeSnippet {
|
||||
pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self {
|
||||
let (content, language_name, file_path) =
|
||||
search_result.buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let content = snapshot
|
||||
.text_for_range(search_result.range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let language_name = buffer
|
||||
.language()
|
||||
.and_then(|language| Some(language.name().to_string()));
|
||||
|
||||
let file_path = buffer
|
||||
.file()
|
||||
.and_then(|file| Some(file.path().to_path_buf()));
|
||||
|
||||
(content, language_name, file_path)
|
||||
});
|
||||
|
||||
PromptCodeSnippet {
|
||||
path: file_path,
|
||||
language_name,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for PromptCodeSnippet {
|
||||
fn to_string(&self) -> String {
|
||||
let path = self
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|path| Some(path.to_string_lossy().to_string()))
|
||||
.unwrap_or("".to_string());
|
||||
let language_name = self.language_name.clone().unwrap_or("".to_string());
|
||||
let content = self.content.clone();
|
||||
|
||||
format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
@ -120,70 +173,135 @@ pub fn generate_content_prompt(
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<impl ToOffset>,
|
||||
kind: CodegenKind,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
) -> String {
|
||||
let mut prompt = String::new();
|
||||
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
|
||||
const RESERVED_TOKENS_FOR_GENERATION: usize = 1000;
|
||||
|
||||
let mut prompts = Vec::new();
|
||||
let range = range.to_offset(buffer);
|
||||
|
||||
// General Preamble
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
|
||||
prompts.push(format!("You're an expert {language_name} engineer.\n"));
|
||||
} else {
|
||||
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
||||
prompts.push("You're an expert engineer.\n".to_string());
|
||||
}
|
||||
|
||||
let outline = summarize(buffer, range);
|
||||
writeln!(
|
||||
prompt,
|
||||
"The file you are currently working on has the following outline:"
|
||||
)
|
||||
.unwrap();
|
||||
// Snippets
|
||||
let mut snippet_position = prompts.len() - 1;
|
||||
|
||||
let mut content = String::new();
|
||||
content.extend(buffer.text_for_range(0..range.start));
|
||||
if range.start == range.end {
|
||||
content.push_str("<|START|>");
|
||||
} else {
|
||||
content.push_str("<|START|");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.clone()));
|
||||
if range.start != range.end {
|
||||
content.push_str("|END|>");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||
|
||||
prompts.push("The file you are currently working on has the following content:\n".to_string());
|
||||
|
||||
if let Some(language_name) = language_name {
|
||||
let language_name = language_name.to_lowercase();
|
||||
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
|
||||
prompts.push(format!("```{language_name}\n{content}\n```"));
|
||||
} else {
|
||||
writeln!(prompt, "```\n{outline}\n```").unwrap();
|
||||
prompts.push(format!("```\n{content}\n```"));
|
||||
}
|
||||
|
||||
match kind {
|
||||
CodegenKind::Generate { position: _ } => {
|
||||
writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|` marker is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string());
|
||||
prompts
|
||||
.push("Assume the cursor is located where the `<|START|` marker is.".to_string());
|
||||
prompts.push(
|
||||
"Text can't be replaced, so assume your answer will be inserted at the cursor."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
.to_string(),
|
||||
);
|
||||
prompts.push(format!(
|
||||
"Generate text based on the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
));
|
||||
}
|
||||
CodegenKind::Transform { range: _ } => {
|
||||
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the users code selected text based upon the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string());
|
||||
prompts.push(format!(
|
||||
"Modify the users code selected text based upon the users prompt: '{user_prompt}'"
|
||||
));
|
||||
prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
|
||||
prompts.push(format!(
|
||||
"Your answer MUST always and only be valid {language_name}"
|
||||
));
|
||||
}
|
||||
writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
prompts.push("Never make remarks about the output.".to_string());
|
||||
prompts.push("Do not return any text, except the generated code.".to_string());
|
||||
prompts.push("Always wrap your code in a Markdown block".to_string());
|
||||
|
||||
prompt
|
||||
let current_messages = [ChatCompletionRequestMessage {
|
||||
role: "user".to_string(),
|
||||
content: Some(prompts.join("\n")),
|
||||
function_call: None,
|
||||
name: None,
|
||||
}];
|
||||
|
||||
let mut remaining_token_count = if let Ok(current_token_count) =
|
||||
tiktoken_rs::num_tokens_from_messages(model, ¤t_messages)
|
||||
{
|
||||
let max_token_count = tiktoken_rs::model::get_context_size(model);
|
||||
let intermediate_token_count = if max_token_count > current_token_count {
|
||||
max_token_count - current_token_count
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION {
|
||||
0
|
||||
} else {
|
||||
intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION
|
||||
}
|
||||
} else {
|
||||
// If tiktoken fails to count token count, assume we have no space remaining.
|
||||
0
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - add repository name to snippet
|
||||
// - add file path
|
||||
// - add language
|
||||
if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) {
|
||||
let mut template = "You are working inside a large repository, here are a few code snippets that may be useful";
|
||||
|
||||
for search_result in search_results {
|
||||
let mut snippet_prompt = template.to_string();
|
||||
let snippet = search_result.to_string();
|
||||
writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap();
|
||||
|
||||
let token_count = encoding
|
||||
.encode_with_special_tokens(snippet_prompt.as_str())
|
||||
.len();
|
||||
if token_count <= remaining_token_count {
|
||||
if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT {
|
||||
prompts.insert(snippet_position, snippet_prompt);
|
||||
snippet_position += 1;
|
||||
remaining_token_count -= token_count;
|
||||
// If you have already added the template to the prompt, remove the template.
|
||||
template = "";
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompts.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -18,7 +18,7 @@ use live_kit_client::{
|
||||
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
|
||||
RemoteVideoTrackUpdate,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
@ -70,6 +70,8 @@ pub struct Room {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
room_update_completed_tx: watch::Sender<Option<()>>,
|
||||
room_update_completed_rx: watch::Receiver<Option<()>>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
}
|
||||
@ -211,6 +213,8 @@ impl Room {
|
||||
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
|
||||
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
|
||||
|
||||
Self {
|
||||
id,
|
||||
channel_id,
|
||||
@ -230,6 +234,8 @@ impl Room {
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
room_update_completed_tx,
|
||||
room_update_completed_rx,
|
||||
}
|
||||
}
|
||||
|
||||
@ -599,7 +605,7 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Returns the most 'active' projects, defined as most people in the project
|
||||
pub fn most_active_project(&self) -> Option<(u64, u64)> {
|
||||
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
|
||||
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||
for participant in self.remote_participants.values() {
|
||||
match participant.location {
|
||||
@ -619,6 +625,15 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
for project in &self.local_participant.projects {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
project_hosts_and_guest_counts
|
||||
.into_iter()
|
||||
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||
@ -858,6 +873,7 @@ impl Room {
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
@ -866,6 +882,17 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
|
||||
let mut done_rx = self.room_update_completed_rx.clone();
|
||||
async move {
|
||||
while let Some(result) = done_rx.next().await {
|
||||
if result.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_video_track_updated(
|
||||
&mut self,
|
||||
change: RemoteVideoTrackUpdate,
|
||||
|
@ -99,6 +99,10 @@ impl ChannelBuffer {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn remote_id(&self, cx: &AppContext) -> u64 {
|
||||
self.buffer.read(cx).remote_id()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
&self.user_store
|
||||
}
|
||||
|
@ -5,10 +5,11 @@ use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
use client::{Client, Subscription, User, UserId, UserStore};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use rpc::{
|
||||
proto::{self, ChannelEdge, ChannelPermission},
|
||||
proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@ -48,17 +49,63 @@ pub type ChannelData = (Channel, ChannelPath);
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
pub unseen_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
+ "channel/"
|
||||
+ &self.slug()
|
||||
+ "-"
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect();
|
||||
|
||||
slug.trim_matches(|c| c == '-').to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ChannelPath(Arc<[ChannelId]>);
|
||||
|
||||
pub struct ChannelMembership {
|
||||
pub user: Arc<User>,
|
||||
pub kind: proto::channel_member::Kind,
|
||||
pub admin: bool,
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
impl ChannelMembership {
|
||||
pub fn sort_key(&self) -> MembershipSortKey {
|
||||
MembershipSortKey {
|
||||
role_order: match self.role {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
proto::channel_member::Kind::AncestorMember => 1,
|
||||
proto::channel_member::Kind::Invitee => 2,
|
||||
},
|
||||
username_order: self.user.github_login.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct MembershipSortKey<'a> {
|
||||
role_order: u8,
|
||||
kind_order: u8,
|
||||
username_order: &'a str,
|
||||
}
|
||||
|
||||
pub enum ChannelEvent {
|
||||
@ -93,12 +140,21 @@ impl ChannelStore {
|
||||
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade(&cx)?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
.await
|
||||
.log_err()?;
|
||||
}
|
||||
client::Status::SignedOut | client::Status::UpgradeRequired => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
|
||||
}
|
||||
_ => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
|
||||
}
|
||||
}
|
||||
if status.is_connected() {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
.await
|
||||
.log_err()?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
@ -415,7 +471,7 @@ impl ChannelStore {
|
||||
insert_edge: parent_edge,
|
||||
channel_permissions: vec![ChannelPermission {
|
||||
channel_id,
|
||||
is_admin: true,
|
||||
role: ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@ -487,11 +543,30 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_channel_visibility(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|_, _| async move {
|
||||
let _ = client
|
||||
.request(proto::SetChannelVisibility {
|
||||
channel_id,
|
||||
visibility: visibility.into(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invite_member(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
@ -505,7 +580,7 @@ impl ChannelStore {
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
@ -549,11 +624,11 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_member_admin(
|
||||
pub fn set_member_role(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
@ -564,10 +639,10 @@ impl ChannelStore {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberAdmin {
|
||||
.request(proto::SetChannelMemberRole {
|
||||
channel_id,
|
||||
user_id,
|
||||
admin,
|
||||
role: role.into(),
|
||||
})
|
||||
.await;
|
||||
|
||||
@ -655,8 +730,8 @@ impl ChannelStore {
|
||||
.filter_map(|(user, member)| {
|
||||
Some(ChannelMembership {
|
||||
user,
|
||||
admin: member.admin,
|
||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
||||
role: member.role(),
|
||||
kind: member.kind(),
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
@ -802,7 +877,7 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
self.channel_participants.clear();
|
||||
@ -813,7 +888,10 @@ impl ChannelStore {
|
||||
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
cx.background().timer(RECONNECT_TIMEOUT).await;
|
||||
if wait_for_reconnect {
|
||||
cx.background().timer(RECONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
@ -848,6 +926,7 @@ impl ChannelStore {
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
name: channel.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
@ -914,7 +993,7 @@ impl ChannelStore {
|
||||
}
|
||||
|
||||
for permission in payload.channel_permissions {
|
||||
if permission.is_admin {
|
||||
if permission.role() == proto::ChannelRole::Admin {
|
||||
self.channels_with_admin_privileges
|
||||
.insert(permission.channel_id);
|
||||
} else {
|
||||
|
@ -123,12 +123,15 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
|
||||
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.name = channel_proto.name;
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
name: channel_proto.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
|
@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||
use rpc::proto;
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 1,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
proto::Channel {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
},
|
||||
],
|
||||
insert_edge: vec![
|
||||
@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
],
|
||||
channel_permissions: vec![proto::ChannelPermission {
|
||||
channel_id: 0,
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
channels: vec![proto::Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
@ -182,6 +182,7 @@ impl Bundle {
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
|
@ -4,7 +4,9 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use sysinfo::{
|
||||
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
@ -166,8 +168,16 @@ impl Telemetry {
|
||||
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||
let refresh_kind = RefreshKind::new()
|
||||
.with_memory() // For memory usage
|
||||
.with_processes(ProcessRefreshKind::everything()) // For process usage
|
||||
.with_cpu(CpuRefreshKind::everything()); // For core count
|
||||
|
||||
let mut system = System::new_with_specifics(refresh_kind);
|
||||
|
||||
// Avoiding calling `refresh_all()`, just update what we need
|
||||
system.refresh_specifics(refresh_kind);
|
||||
|
||||
loop {
|
||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||
@ -175,8 +185,7 @@ impl Telemetry {
|
||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
system.refresh_memory();
|
||||
system.refresh_processes();
|
||||
system.refresh_specifics(refresh_kind);
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
let Some(process) = system.processes().get(¤t_process) else {
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.23.3"
|
||||
version = "0.25.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
|
@ -37,12 +37,14 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
CREATE TABLE "rooms" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"live_kit_room" VARCHAR NOT NULL,
|
||||
"enviroment" VARCHAR,
|
||||
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
|
||||
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"host_connection_id" INTEGER,
|
||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||
@ -190,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
CREATE TABLE "channels" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"visibility" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
@ -224,6 +227,7 @@ CREATE TABLE "channel_members" (
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"role" VARCHAR,
|
||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
||||
);
|
||||
|
@ -0,0 +1 @@
|
||||
ALTER TABLE rooms ADD COLUMN enviroment TEXT;
|
@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
@ -0,0 +1,4 @@
|
||||
ALTER TABLE channel_members ADD COLUMN role TEXT;
|
||||
UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END;
|
||||
|
||||
ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members';
|
@ -0,0 +1,8 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE projects
|
||||
DROP CONSTRAINT projects_room_id_fkey,
|
||||
ADD CONSTRAINT projects_room_id_fkey
|
||||
FOREIGN KEY (room_id)
|
||||
REFERENCES rooms (id)
|
||||
ON DELETE CASCADE;
|
@ -432,6 +432,7 @@ pub struct NewUserResult {
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::Result;
|
||||
use rpc::proto;
|
||||
use sea_orm::{entity::prelude::*, DbErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -80,3 +81,101 @@ id_type!(SignupId);
|
||||
id_type!(UserId);
|
||||
id_type!(ChannelBufferCollaboratorId);
|
||||
id_type!(FlagId);
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "admin")]
|
||||
Admin,
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
#[sea_orm(string_value = "banned")]
|
||||
Banned,
|
||||
}
|
||||
|
||||
impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max(&self, other: Self) -> Self {
|
||||
if self.should_override(other) {
|
||||
*self
|
||||
} else {
|
||||
other
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ChannelRole> for ChannelRole {
|
||||
fn from(value: proto::ChannelRole) -> Self {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelRole> for ChannelRole {
|
||||
fn into(self) -> proto::ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelRole {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelRole = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||
pub enum ChannelVisibility {
|
||||
#[sea_orm(string_value = "public")]
|
||||
Public,
|
||||
#[sea_orm(string_value = "members")]
|
||||
#[default]
|
||||
Members,
|
||||
}
|
||||
|
||||
impl From<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn from(value: proto::ChannelVisibility) -> Self {
|
||||
match value {
|
||||
proto::ChannelVisibility::Public => ChannelVisibility::Public,
|
||||
proto::ChannelVisibility::Members => ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<proto::ChannelVisibility> for ChannelVisibility {
|
||||
fn into(self) -> proto::ChannelVisibility {
|
||||
match self {
|
||||
ChannelVisibility::Public => proto::ChannelVisibility::Public,
|
||||
ChannelVisibility::Members => proto::ChannelVisibility::Members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for ChannelVisibility {
|
||||
fn into(self) -> i32 {
|
||||
let proto: proto::ChannelVisibility = self.into();
|
||||
proto.into()
|
||||
}
|
||||
}
|
||||
|
@ -482,7 +482,9 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -180,7 +180,9 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
let mut channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok((
|
||||
@ -337,8 +339,22 @@ impl Database {
|
||||
.filter(channel_message::Column::SenderId.eq(user_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("no such message"))?;
|
||||
if self
|
||||
.check_user_is_channel_admin(channel_id, user_id, &*tx)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
let result = channel_message::Entity::delete_by_id(message_id)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("no such message"))?;
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("operation could not be completed"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(participant_connection_ids)
|
||||
|
@ -53,7 +53,9 @@ impl Database {
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members;
|
||||
if let Some(channel_id) = channel_id {
|
||||
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
|
||||
channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
} else {
|
||||
channel_members = Vec::new();
|
||||
|
||||
@ -107,10 +109,12 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
release_channel: &str,
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
enviroment: ActiveValue::set(Some(release_channel.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@ -270,112 +274,165 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
enviroment: &str,
|
||||
) -> Result<RoomGuard<JoinRoom>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelId {
|
||||
enum QueryChannelIdAndEnviroment {
|
||||
ChannelId,
|
||||
Enviroment,
|
||||
}
|
||||
let channel_id: Option<ChannelId> = room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelId>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
|
||||
room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.column(room::Column::Enviroment)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelIdAndEnviroment>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if let Some(release_channel) = release_channel {
|
||||
if &release_channel != enviroment {
|
||||
Err(anyhow!("must join using the {} release", release_channel))?;
|
||||
}
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
|
||||
if channel_id.is_some() {
|
||||
Err(anyhow!("tried to join channel call directly"))?
|
||||
}
|
||||
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(user_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::UserId.eq(user_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
if result.rows_affected == 0 {
|
||||
Err(anyhow!("room does not exist or was already joined"))?;
|
||||
}
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id,
|
||||
channel_members,
|
||||
channel_id: None,
|
||||
channel_members: vec![],
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_next_participant_index_internal(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<i32> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
Ok(participant_index)
|
||||
}
|
||||
|
||||
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
|
||||
self.transaction(|tx| async move {
|
||||
let room: Option<room::Model> = room::Entity::find()
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(room.and_then(|room| room.channel_id))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn join_channel_room_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<JoinRoom> {
|
||||
let participant_index = self
|
||||
.get_next_participant_index_internal(room_id, &*tx)
|
||||
.await?;
|
||||
|
||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
calling_user_id: ActiveValue::set(user_id),
|
||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
OnConflict::columns([room_participant::Column::UserId])
|
||||
.update_columns([
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let channel_members = self
|
||||
.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?;
|
||||
Ok(JoinRoom {
|
||||
room,
|
||||
channel_id: Some(channel_id),
|
||||
channel_members,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn rejoin_room(
|
||||
&self,
|
||||
rejoin_room: proto::RejoinRoom,
|
||||
@ -667,7 +724,8 @@ impl Database {
|
||||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@ -818,17 +876,15 @@ impl Database {
|
||||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let deleted = if room.participants.is_empty() {
|
||||
let result = room::Entity::delete_by_id(room_id)
|
||||
.filter(room::Column::ChannelId.is_null())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
result.rows_affected > 0
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let channel_members = if let Some(channel_id) = channel_id {
|
||||
self.get_channel_members_internal(channel_id, &tx).await?
|
||||
self.get_channel_participants_internal(channel_id, &tx)
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::db::ChannelId;
|
||||
use crate::db::{ChannelId, ChannelVisibility};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
@ -7,6 +7,7 @@ pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
|
||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "channel_members")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
@ -9,7 +9,7 @@ pub struct Model {
|
||||
pub channel_id: ChannelId,
|
||||
pub user_id: UserId,
|
||||
pub accepted: bool,
|
||||
pub admin: bool,
|
||||
pub role: ChannelRole,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -8,6 +8,7 @@ pub struct Model {
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
pub enviroment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
use std::sync::Arc;
|
||||
|
||||
const TEST_RELEASE_CHANNEL: &'static str = "test";
|
||||
|
||||
pub struct TestDb {
|
||||
pub db: Option<Arc<Database>>,
|
||||
pub connection: Option<sqlx::AnyConnection>,
|
||||
@ -157,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)
|
||||
graph.channels.push(Channel {
|
||||
id: *id,
|
||||
name: name.to_string(),
|
||||
visibility: ChannelVisibility::Members,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -54,9 +54,9 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
let owner_id = db.create_server("production").await.unwrap().0 as u32;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
assert_eq!(left_buffer.connections, &[connection_id_a],);
|
||||
|
||||
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
||||
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
|
||||
let _ = db
|
||||
.join_channel_buffer(cargo_id, a_id, connection_id_a)
|
||||
.await
|
||||
@ -207,11 +207,11 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
let mut text_buffers = Vec::new();
|
||||
for i in 0..3 {
|
||||
let channel = db
|
||||
.create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id)
|
||||
.create_root_channel(&format!("channel-{i}"), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel, observer_id, user_id, false)
|
||||
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(channel, observer_id, true)
|
||||
|
@ -5,10 +5,17 @@ use rpc::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
|
||||
db::{
|
||||
queries::channels::ChannelGraph,
|
||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||
ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicI32, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||
|
||||
@ -41,12 +48,12 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(zed_id, b_id).await.is_err());
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -54,16 +61,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(zed_id), "3", a_id)
|
||||
.create_channel("livestreaming", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let replace_id = db
|
||||
.create_channel("replace", Some(zed_id), "4", a_id)
|
||||
.create_channel("replace", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -71,14 +75,14 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
members.sort();
|
||||
assert_eq!(members, &[a_id, b_id]);
|
||||
|
||||
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
|
||||
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
|
||||
let cargo_id = db
|
||||
.create_channel("cargo", Some(rust_id), "6", a_id)
|
||||
.create_channel("cargo", Some(rust_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cargo_ra_id = db
|
||||
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
|
||||
.create_channel("cargo-ra", Some(cargo_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -124,9 +128,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
);
|
||||
|
||||
// Update member permissions
|
||||
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
|
||||
let set_subchannel_admin = db
|
||||
.set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_subchannel_admin.is_err());
|
||||
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
|
||||
let set_channel_admin = db
|
||||
.set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
|
||||
.await;
|
||||
assert!(set_channel_admin.is_ok());
|
||||
|
||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||
@ -149,7 +157,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
|
||||
// Remove a single channel
|
||||
db.delete_channel(crdb_id, a_id).await.unwrap();
|
||||
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(crdb_id, a_id).await.is_err());
|
||||
|
||||
// Remove a channel tree
|
||||
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
|
||||
@ -157,9 +165,9 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
||||
assert_eq!(user_ids, &[a_id]);
|
||||
|
||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
||||
assert!(db.get_channel(rust_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_id, a_id).await.is_err());
|
||||
assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
@ -198,23 +206,30 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
|
||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
|
||||
let (joined_room, _) = db
|
||||
.join_channel(
|
||||
channel_1,
|
||||
user_1,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
|
||||
let room_id = RoomId::from_proto(joined_room.room.id);
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
|
||||
.join_room(
|
||||
room_id,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
@ -228,64 +243,21 @@ test_both_dbs!(
|
||||
async fn test_channel_invites(db: &Arc<Database>) {
|
||||
db.create_server("test").await.unwrap();
|
||||
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user2@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user2".into(),
|
||||
github_user_id: 6,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_1 = new_test_user(db, "user1@example.com").await;
|
||||
let user_2 = new_test_user(db, "user2@example.com").await;
|
||||
let user_3 = new_test_user(db, "user3@example.com").await;
|
||||
|
||||
let user_3 = db
|
||||
.create_user(
|
||||
"user3@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user3".into(),
|
||||
github_user_id: 7,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
let channel_1_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channel_1_2 = db
|
||||
.create_root_channel("channel_2", "2", user_1)
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_2, user_2, user_1, false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, true)
|
||||
db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -309,27 +281,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
|
||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_1, user_1)
|
||||
let mut members = db
|
||||
.get_channel_participant_details(channel_1_1, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@ -339,12 +313,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let channel_1_3 = db
|
||||
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
|
||||
.create_channel("channel_3", Some(channel_1_1), user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let members = db
|
||||
.get_channel_member_details(channel_1_3, user_1)
|
||||
.get_channel_participant_details(channel_1_3, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@ -353,12 +327,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
admin: false,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
@ -401,7 +375,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
|
||||
|
||||
db.rename_channel(zed_id, user_1, "#zed-archive")
|
||||
.await
|
||||
@ -409,11 +383,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
|
||||
let zed_archive_id = zed_id;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(zed_archive_id, user_1)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let channel = db.get_channel(zed_archive_id, user_1).await.unwrap();
|
||||
assert_eq!(channel.name, "zed-archive");
|
||||
|
||||
let non_permissioned_rename = db
|
||||
@ -446,25 +416,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
|
||||
let gpui2_id = db
|
||||
.create_channel("gpui2", Some(zed_id), "3", a_id)
|
||||
.create_channel("gpui2", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
|
||||
.create_channel("livestreaming", Some(crdb_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_dag_id = db
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -517,12 +484,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
// ========================================================================
|
||||
// Create a new channel below a channel with multiple parents
|
||||
let livestreaming_dag_sub_id = db
|
||||
.create_channel(
|
||||
"livestreaming_dag_sub",
|
||||
Some(livestreaming_dag_id),
|
||||
"6",
|
||||
a_id,
|
||||
)
|
||||
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -812,15 +774,15 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
|
||||
|
||||
let projects_id = db
|
||||
.create_channel("projects", Some(zed_id), "2", user_id)
|
||||
.create_channel("projects", Some(zed_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(projects_id), "3", user_id)
|
||||
.create_channel("livestreaming", Some(projects_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -849,6 +811,284 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_is_channel_participant,
|
||||
test_user_is_channel_participant_postgres,
|
||||
test_user_is_channel_participant_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
let member = new_test_user(db, "member@example.com").await;
|
||||
let guest = new_test_user(db, "guest@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(active_channel, member, admin, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(active_channel, member, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, admin, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, member, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(vim_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(channels, &[(vim_channel, None)]);
|
||||
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(active_channel, None), (vim_channel, Some(active_channel))],
|
||||
);
|
||||
|
||||
db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Banned.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.remove_channel_member(vim_channel, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(zed_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(active_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.is_err(),);
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
members.sort_by_key(|member| member.user_id);
|
||||
|
||||
assert_eq!(
|
||||
members,
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_dag(
|
||||
channels,
|
||||
&[(zed_channel, None), (vim_channel, Some(zed_channel))],
|
||||
)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_joins_correct_channel,
|
||||
test_user_joins_correct_channel_postgres,
|
||||
test_user_joins_correct_channel_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
|
||||
let active_channel = db
|
||||
.create_channel("active", Some(zed_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim_channel = db
|
||||
.create_channel("vim", Some(active_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim2_channel = db
|
||||
.create_channel("vim2", Some(vim_channel), admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let most_public = db
|
||||
.transaction(
|
||||
|tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(most_public, Some(zed_channel))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||
@ -873,3 +1113,20 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)])
|
||||
|
||||
pretty_assertions::assert_eq!(actual_map, expected_map)
|
||||
}
|
||||
|
||||
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||
|
||||
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
|
||||
db.create_user(
|
||||
email,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: email[0..email.find("@").unwrap()].to_string(),
|
||||
github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id
|
||||
}
|
||||
|
@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let room_id = RoomId::from_proto(
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(
|
||||
room_id,
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"dev",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||
|
||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
||||
@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() {
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_non_matching_release_channels,
|
||||
test_non_matching_release_channels_postgres,
|
||||
test_non_matching_release_channels_sqlite
|
||||
);
|
||||
|
||||
async fn test_non_matching_release_channels(db: &Arc<Database>) {
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
let user1 = db
|
||||
.create_user(
|
||||
&format!("admin@example.com"),
|
||||
true,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
github_user_id: 0,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user2 = db
|
||||
.create_user(
|
||||
&format!("user@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room = db
|
||||
.create_room(
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
"",
|
||||
"stable",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.call(
|
||||
RoomId::from_proto(room.id),
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
user2.user_id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User attempts to join from preview
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"preview",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// User switches to stable
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"stable",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok())
|
||||
}
|
||||
|
||||
fn build_background_executor() -> Arc<Background> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
db::{Database, MessageId, NewUserParams},
|
||||
db::{ChannelRole, Database, MessageId, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@ -25,10 +25,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||
@ -90,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
@ -157,17 +151,11 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let channel_2 = db
|
||||
.create_channel("channel-2", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1, observer, user, false)
|
||||
db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -175,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_2, observer, user, false)
|
||||
db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -3,8 +3,8 @@ mod connection_pool;
|
||||
use crate::{
|
||||
auth,
|
||||
db::{
|
||||
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
|
||||
ServerId, User, UserId,
|
||||
self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId,
|
||||
ProjectId, RoomId, ServerId, User, UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
@ -63,6 +63,7 @@ use time::OffsetDateTime;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
use util::channel::RELEASE_CHANNEL_NAME;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@ -224,6 +225,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
|
||||
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
|
||||
.add_request_handler(forward_project_request::<proto::PrepareRename>)
|
||||
@ -253,7 +255,8 @@ impl Server {
|
||||
.add_request_handler(delete_channel)
|
||||
.add_request_handler(invite_channel_member)
|
||||
.add_request_handler(remove_channel_member)
|
||||
.add_request_handler(set_channel_member_admin)
|
||||
.add_request_handler(set_channel_member_role)
|
||||
.add_request_handler(set_channel_visibility)
|
||||
.add_request_handler(rename_channel)
|
||||
.add_request_handler(join_channel_buffer)
|
||||
.add_request_handler(leave_channel_buffer)
|
||||
@ -937,11 +940,6 @@ async fn create_room(
|
||||
util::async_iife!({
|
||||
let live_kit = live_kit?;
|
||||
|
||||
live_kit
|
||||
.create_room(live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()?;
|
||||
|
||||
let token = live_kit
|
||||
.room_token(&live_kit_room, &session.user_id.to_string())
|
||||
.trace_err()?;
|
||||
@ -957,7 +955,12 @@ async fn create_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.create_room(
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
&live_kit_room,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
@ -975,26 +978,28 @@ async fn join_room(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let room_id = RoomId::from_proto(request.id);
|
||||
|
||||
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
return join_channel_internal(channel_id, Box::new(response), session).await;
|
||||
}
|
||||
|
||||
let joined_room = {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
room_updated(&room.room, &session.peer);
|
||||
room.into_inner()
|
||||
};
|
||||
|
||||
if let Some(channel_id) = joined_room.channel_id {
|
||||
channel_updated(
|
||||
channel_id,
|
||||
&joined_room.room,
|
||||
&joined_room.channel_members,
|
||||
&session.peer,
|
||||
&*session.connection_pool().await,
|
||||
)
|
||||
}
|
||||
|
||||
for connection_id in session
|
||||
.connection_pool()
|
||||
.await
|
||||
@ -1032,7 +1037,7 @@ async fn join_room(
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
room: Some(joined_room.room),
|
||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
||||
channel_id: None,
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
@ -2195,20 +2200,16 @@ async fn create_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
live_kit.create_room(live_kit_room.clone()).await?;
|
||||
}
|
||||
|
||||
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
|
||||
let id = db
|
||||
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
|
||||
.create_channel(&request.name, parent_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
id: id.to_proto(),
|
||||
name: request.name,
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
};
|
||||
|
||||
response.send(proto::CreateChannelResponse {
|
||||
@ -2281,17 +2282,20 @@ async fn invite_channel_member(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let invitee_id = UserId::from_proto(request.user_id);
|
||||
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
|
||||
.await?;
|
||||
db.invite_channel_member(
|
||||
channel_id,
|
||||
invitee_id,
|
||||
session.user_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, _) = db
|
||||
.get_channel(channel_id, session.user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
});
|
||||
for connection_id in session
|
||||
@ -2333,27 +2337,63 @@ async fn remove_channel_member(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_admin(
|
||||
request: proto::SetChannelMemberAdmin,
|
||||
response: Response<proto::SetChannelMemberAdmin>,
|
||||
async fn set_channel_visibility(
|
||||
request: proto::SetChannelVisibility,
|
||||
response: Response<proto::SetChannelVisibility>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let visibility = request.visibility().into();
|
||||
|
||||
let channel = db
|
||||
.set_channel_visibility(channel_id, visibility, session.user_id)
|
||||
.await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
});
|
||||
|
||||
let member_ids = db.get_channel_members(channel_id).await?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for member_id in member_ids {
|
||||
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel_member_role(
|
||||
request: proto::SetChannelMemberRole,
|
||||
response: Response<proto::SetChannelMemberRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let member_id = UserId::from_proto(request.user_id);
|
||||
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
|
||||
let channel_member = db
|
||||
.set_channel_member_role(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
member_id,
|
||||
request.role().into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (channel, has_accepted) = db
|
||||
.get_channel(channel_id, member_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
||||
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
if has_accepted {
|
||||
if channel_member.accepted {
|
||||
update.channel_permissions.push(proto::ChannelPermission {
|
||||
channel_id: channel.id.to_proto(),
|
||||
is_admin: request.admin,
|
||||
role: request.role,
|
||||
});
|
||||
}
|
||||
|
||||
@ -2376,13 +2416,14 @@ async fn rename_channel(
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let new_name = db
|
||||
let channel = db
|
||||
.rename_channel(channel_id, session.user_id, &request.name)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
id: request.channel_id,
|
||||
name: new_name,
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
};
|
||||
response.send(proto::RenameChannelResponse {
|
||||
channel: Some(channel.clone()),
|
||||
@ -2420,6 +2461,7 @@ async fn link_channel(
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
@ -2511,6 +2553,7 @@ async fn move_channel(
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
})
|
||||
.collect(),
|
||||
@ -2536,7 +2579,7 @@ async fn get_channel_members(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let members = db
|
||||
.get_channel_member_details(channel_id, session.user_id)
|
||||
.get_channel_participant_details(channel_id, session.user_id)
|
||||
.await?;
|
||||
response.send(proto::GetChannelMembersResponse { members })?;
|
||||
Ok(())
|
||||
@ -2552,53 +2595,68 @@ async fn respond_to_channel_invite(
|
||||
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
||||
.await?;
|
||||
|
||||
if request.accept {
|
||||
channel_membership_updated(db, channel_id, &session).await?;
|
||||
} else {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
}
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn channel_membership_updated(
|
||||
db: tokio::sync::MutexGuard<'_, DbHandle>,
|
||||
channel_id: ChannelId,
|
||||
session: &Session,
|
||||
) -> Result<(), crate::Error> {
|
||||
let mut update = proto::UpdateChannels::default();
|
||||
update
|
||||
.remove_channel_invitations
|
||||
.push(channel_id.to_proto());
|
||||
if request.accept {
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update
|
||||
.channels
|
||||
.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
is_admin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||
update.channels.extend(
|
||||
result
|
||||
.channels
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|channel| proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
visibility: channel.visibility.into(),
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
.extend(
|
||||
result
|
||||
.channel_participants
|
||||
.into_iter()
|
||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||
channel_id: channel_id.to_proto(),
|
||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||
}),
|
||||
);
|
||||
update
|
||||
.channel_permissions
|
||||
.extend(
|
||||
result
|
||||
.channels_with_admin_privileges
|
||||
.into_iter()
|
||||
.map(|channel_id| proto::ChannelPermission {
|
||||
channel_id: channel_id.to_proto(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
session.peer.send(session.connection_id, update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -2608,15 +2666,39 @@ async fn join_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
join_channel_internal(channel_id, Box::new(response), session).await
|
||||
}
|
||||
|
||||
trait JoinChannelInternalResponse {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinChannel>::send(self, result)
|
||||
}
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinRoom>::send(self, result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: ChannelId,
|
||||
response: Box<impl JoinChannelInternalResponse>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db.room_id_for_channel(channel_id).await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
let (joined_room, joined_channel) = db
|
||||
.join_channel(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
@ -2639,9 +2721,13 @@ async fn join_channel(
|
||||
live_kit_connection_info,
|
||||
})?;
|
||||
|
||||
if let Some(joined_channel) = joined_channel {
|
||||
channel_membership_updated(db, joined_channel, &session).await?
|
||||
}
|
||||
|
||||
room_updated(&joined_room.room, &session.peer);
|
||||
|
||||
joined_room.into_inner()
|
||||
joined_room
|
||||
};
|
||||
|
||||
channel_updated(
|
||||
@ -2653,7 +2739,6 @@ async fn join_channel(
|
||||
);
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -3063,6 +3148,7 @@ fn build_initial_channels_update(
|
||||
update.channels.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
visibility: channel.visibility.into(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -3087,7 +3173,7 @@ fn build_initial_channels_update(
|
||||
.into_iter()
|
||||
.map(|id| proto::ChannelPermission {
|
||||
channel_id: id.to_proto(),
|
||||
is_admin: true,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
}),
|
||||
);
|
||||
|
||||
@ -3095,6 +3181,8 @@ fn build_initial_channels_update(
|
||||
update.channel_invitations.push(proto::Channel {
|
||||
id: channel.id.to_proto(),
|
||||
name: channel.name,
|
||||
// TODO: Visibility
|
||||
visibility: ChannelVisibility::Public.into(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,10 @@ use collections::HashMap;
|
||||
use editor::{Anchor, Editor, ToOffset};
|
||||
use futures::future;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
|
||||
Channel {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
visibility: proto::ChannelVisibility::Members,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
@ -68,7 +71,12 @@ async fn test_core_channels(
|
||||
.update(cx_a, |store, cx| {
|
||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
|
||||
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
||||
let invite = store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Make sure we're synchronously storing the pending invite
|
||||
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||
@ -103,12 +111,12 @@ async fn test_core_channels(
|
||||
&[
|
||||
(
|
||||
client_a.user_id().unwrap(),
|
||||
true,
|
||||
proto::ChannelRole::Admin,
|
||||
proto::channel_member::Kind::Member,
|
||||
),
|
||||
(
|
||||
client_b.user_id().unwrap(),
|
||||
false,
|
||||
proto::ChannelRole::Member,
|
||||
proto::channel_member::Kind::Invitee,
|
||||
),
|
||||
],
|
||||
@ -183,7 +191,12 @@ async fn test_core_channels(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
|
||||
store.set_member_role(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
|
||||
#[track_caller]
|
||||
fn assert_members_eq(
|
||||
members: &[ChannelMembership],
|
||||
expected_members: &[(u64, bool, proto::channel_member::Kind)],
|
||||
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
|
||||
) {
|
||||
assert_eq!(
|
||||
members
|
||||
.iter()
|
||||
.map(|member| (member.user.id, member.admin, member.kind))
|
||||
.map(|member| (member.user.id, member.role, member.kind))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_members
|
||||
);
|
||||
@ -380,6 +393,8 @@ async fn test_channel_room(
|
||||
|
||||
// Give everyone a chance to observe user A joining
|
||||
deterministic.run_until_parked();
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
@ -609,7 +624,12 @@ async fn test_permissions_update_while_invited(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@ -632,7 +652,12 @@ async fn test_permissions_update_while_invited(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
|
||||
channel_store.set_member_role(
|
||||
rust_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@ -801,7 +826,12 @@ async fn test_lost_channel_creation(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@ -882,6 +912,119 @@ async fn test_lost_channel_creation(
|
||||
],
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_guest_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(&[("channel-a", None)], (&client_a, cx_a))
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert!(client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, _| channel_store
|
||||
.channel_for_id(channel_a_id)
|
||||
.is_some()));
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_a_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invite_access(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(
|
||||
&[("channel-a", None), ("channel-b", Some("channel-a"))],
|
||||
(&client_a, cx_a),
|
||||
)
|
||||
.await;
|
||||
let channel_a_id = channels[0];
|
||||
let channel_b_id = channels[0];
|
||||
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_a_id,
|
||||
client_b.user_id().unwrap(),
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
assert!(channel_store.channel_for_id(channel_b_id).is_some());
|
||||
assert!(channel_store.channel_for_id(channel_a_id).is_some());
|
||||
});
|
||||
|
||||
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||
let participants = channel_store.channel_participants(channel_b_id);
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_moving(
|
||||
|
@ -15,12 +15,14 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||
Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use lsp::LanguageServerId;
|
||||
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
|
||||
use project::{
|
||||
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@ -4407,8 +4409,6 @@ async fn test_formatting_buffer(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
use project::FormatTrigger;
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@ -4511,6 +4511,134 @@ async fn test_formatting_buffer(
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_prettier_formatting_buffer(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
// Set up a fake language server.
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let test_plugin = "test_plugin";
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: vec![test_plugin],
|
||||
}],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry().add(Arc::clone(&language));
|
||||
|
||||
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||
// because later we'll invoke a command, which requires passing a working directory
|
||||
// that points to a valid location on disk.
|
||||
let directory = env::current_dir().unwrap();
|
||||
let buffer_text = "let one = \"two\"";
|
||||
client_a
|
||||
.fs()
|
||||
.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_a.update(cx_a, |project, _| {
|
||||
let suffix = project.enable_test_prettier(&[test_plugin]);
|
||||
project.languages().add(language);
|
||||
suffix
|
||||
});
|
||||
let buffer_a = cx_a
|
||||
.background()
|
||||
.spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let buffer_b = cx_b
|
||||
.background()
|
||||
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::Auto);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(Formatter::LanguageServer);
|
||||
});
|
||||
});
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||
panic!(
|
||||
"Unexpected: prettier should be preferred since it's enabled and language supports it"
|
||||
)
|
||||
});
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(
|
||||
HashSet::from_iter([buffer_b.clone()]),
|
||||
true,
|
||||
FormatTrigger::Save,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
cx_b.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||
buffer_text.to_string() + "\n" + prettier_format_suffix,
|
||||
"Prettier formatting was not applied to client buffer after client's request"
|
||||
);
|
||||
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.format(
|
||||
HashSet::from_iter([buffer_a.clone()]),
|
||||
true,
|
||||
FormatTrigger::Manual,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
cx_b.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||||
"Prettier formatting was not applied to client buffer after host's request"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_definition(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
@ -1,3 +1,5 @@
|
||||
use crate::db::ChannelRole;
|
||||
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@ -46,16 +48,11 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
let db = &server.app_state.db;
|
||||
for ix in 0..CHANNEL_COUNT {
|
||||
let id = db
|
||||
.create_channel(
|
||||
&format!("channel-{ix}"),
|
||||
None,
|
||||
&format!("livekit-room-{ix}"),
|
||||
users[0].user_id,
|
||||
)
|
||||
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
for user in &users[1..] {
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, false)
|
||||
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(id, user.user_id, true)
|
||||
|
@ -15,9 +15,10 @@ use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@ -218,6 +219,7 @@ impl TestServer {
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
background_actions: || &[],
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
@ -325,7 +327,7 @@ impl TestServer {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
member_client.user_id().unwrap(),
|
||||
false,
|
||||
ChannelRole::Member,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@ -567,6 +569,7 @@ impl TestClient {
|
||||
cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client().clone(),
|
||||
self.app_state.node_runtime.clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
@ -613,7 +616,12 @@ impl TestClient {
|
||||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
channel_store.invite_member(
|
||||
channel,
|
||||
other_client.user_id().unwrap(),
|
||||
ChannelRole::Admin,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -24,7 +24,7 @@ use workspace::{
|
||||
item::{FollowableItem, Item, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(channel_view, [Deploy]);
|
||||
@ -93,15 +93,36 @@ impl ChannelView {
|
||||
}
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.items_of_type::<Self>()
|
||||
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
|
||||
.unwrap_or_else(|| {
|
||||
cx.add_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
})
|
||||
})
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
let view = cx.add_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
view
|
||||
})
|
||||
.ok_or_else(|| anyhow!("pane was dropped"))
|
||||
})
|
||||
@ -285,10 +306,14 @@ impl FollowableItem for ChannelView {
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
let channel = self.channel_buffer.read(cx).channel();
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel.id,
|
||||
channel_id: channel_buffer.channel().id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
|
@ -355,8 +355,12 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let (message, is_continuation, is_last) = {
|
||||
let (message, is_continuation, is_last, is_admin) = {
|
||||
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
||||
let is_admin = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.is_user_admin(active_chat.channel().id);
|
||||
let last_message = active_chat.message(ix.saturating_sub(1));
|
||||
let this_message = active_chat.message(ix);
|
||||
let is_continuation = last_message.id != this_message.id
|
||||
@ -366,6 +370,7 @@ impl ChatPanel {
|
||||
active_chat.message(ix).clone(),
|
||||
is_continuation,
|
||||
active_chat.message_count() == ix + 1,
|
||||
is_admin,
|
||||
)
|
||||
};
|
||||
|
||||
@ -386,12 +391,13 @@ impl ChatPanel {
|
||||
};
|
||||
|
||||
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
||||
let message_id_to_remove =
|
||||
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
|
||||
(message.id, belongs_to_user || is_admin)
|
||||
{
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
enum MessageBackgroundHighlight {}
|
||||
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||
|
@ -11,7 +11,10 @@ use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
|
||||
use channel_modal::ChannelModal;
|
||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, Contact, User, UserStore,
|
||||
};
|
||||
use contact_finder::ContactFinder;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
@ -34,8 +37,8 @@ use gpui::{
|
||||
},
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
|
||||
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{Fs, Project};
|
||||
@ -100,6 +103,11 @@ pub struct JoinChannelChat {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct CopyChannelLink {
|
||||
pub channel_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
struct StartMoveChannelFor {
|
||||
channel_id: ChannelId,
|
||||
@ -157,6 +165,7 @@ impl_actions!(
|
||||
OpenChannelNotes,
|
||||
JoinChannelCall,
|
||||
JoinChannelChat,
|
||||
CopyChannelLink,
|
||||
LinkChannel,
|
||||
StartMoveChannelFor,
|
||||
StartLinkChannelFor,
|
||||
@ -205,6 +214,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabPanel::expand_selected_channel);
|
||||
cx.add_action(CollabPanel::open_channel_notes);
|
||||
cx.add_action(CollabPanel::join_channel_chat);
|
||||
cx.add_action(CollabPanel::copy_channel_link);
|
||||
|
||||
cx.add_action(
|
||||
|panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
|
||||
@ -421,7 +431,7 @@ enum ListEntry {
|
||||
is_last: bool,
|
||||
},
|
||||
ParticipantScreen {
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
},
|
||||
IncomingRequest(Arc<User>),
|
||||
@ -435,6 +445,9 @@ enum ListEntry {
|
||||
ChannelNotes {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelChat {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelEditor {
|
||||
depth: usize,
|
||||
},
|
||||
@ -595,6 +608,13 @@ impl CollabPanel {
|
||||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
|
||||
*channel_id,
|
||||
&theme.collab_panel,
|
||||
is_selected,
|
||||
ix,
|
||||
cx,
|
||||
),
|
||||
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
||||
channel.clone(),
|
||||
this.channel_store.clone(),
|
||||
@ -797,7 +817,8 @@ impl CollabPanel {
|
||||
let room = room.read(cx);
|
||||
|
||||
if let Some(channel_id) = room.channel_id() {
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id })
|
||||
}
|
||||
|
||||
// Populate the active user.
|
||||
@ -829,7 +850,13 @@ impl CollabPanel {
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: user_id,
|
||||
is_last: projects.peek().is_none(),
|
||||
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
|
||||
});
|
||||
}
|
||||
if room.is_screen_sharing() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: None,
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -873,7 +900,7 @@ impl CollabPanel {
|
||||
}
|
||||
if !participant.video_tracks.is_empty() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: participant.peer_id,
|
||||
peer_id: Some(participant.peer_id),
|
||||
is_last: true,
|
||||
});
|
||||
}
|
||||
@ -1218,14 +1245,18 @@ impl CollabPanel {
|
||||
) -> AnyElement<Self> {
|
||||
enum CallParticipant {}
|
||||
enum CallParticipantTooltip {}
|
||||
enum LeaveCallButton {}
|
||||
enum LeaveCallTooltip {}
|
||||
|
||||
let collab_theme = &theme.collab_panel;
|
||||
|
||||
let is_current_user =
|
||||
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
||||
|
||||
let content =
|
||||
MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
let content = MouseEventHandler::new::<CallParticipant, _>(
|
||||
user.id as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let style = if is_current_user {
|
||||
*collab_theme
|
||||
.contact_row
|
||||
@ -1261,14 +1292,32 @@ impl CollabPanel {
|
||||
Label::new("Calling", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
.aligned()
|
||||
.into_any(),
|
||||
)
|
||||
} else if is_current_user {
|
||||
Some(
|
||||
Label::new("You", collab_theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(collab_theme.calling_indicator.container)
|
||||
.aligned(),
|
||||
MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
})
|
||||
.with_tooltip::<LeaveCallTooltip>(
|
||||
0,
|
||||
"Leave call",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@ -1277,7 +1326,8 @@ impl CollabPanel {
|
||||
.with_height(collab_theme.row_height)
|
||||
.contained()
|
||||
.with_style(style)
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if is_current_user || is_pending || peer_id.is_none() {
|
||||
return content.into_any();
|
||||
@ -1399,7 +1449,7 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn render_participant_screen(
|
||||
peer_id: PeerId,
|
||||
peer_id: Option<PeerId>,
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::CollabPanel,
|
||||
@ -1414,8 +1464,8 @@ impl CollabPanel {
|
||||
.unwrap_or(0.);
|
||||
let tree_branch = theme.tree_branch;
|
||||
|
||||
MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||
peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
@ -1453,16 +1503,20 @@ impl CollabPanel {
|
||||
.contained()
|
||||
.with_style(row.container)
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
);
|
||||
if peer_id.is_none() {
|
||||
return handler.into_any();
|
||||
}
|
||||
handler
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
@ -1489,23 +1543,32 @@ impl CollabPanel {
|
||||
enum AddChannel {}
|
||||
|
||||
let tooltip_style = &theme.tooltip;
|
||||
let mut channel_link = None;
|
||||
let mut channel_tooltip_text = None;
|
||||
let mut channel_icon = None;
|
||||
|
||||
let text = match section {
|
||||
Section::ActiveCall => {
|
||||
let channel_name = iife!({
|
||||
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
||||
|
||||
let name = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)?
|
||||
.name
|
||||
.as_str();
|
||||
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
|
||||
|
||||
Some(name)
|
||||
channel_link = Some(channel.link());
|
||||
(channel_icon, channel_tooltip_text) = match channel.visibility {
|
||||
proto::ChannelVisibility::Public => {
|
||||
(Some("icons/public.svg"), Some("Copy public channel link."))
|
||||
}
|
||||
proto::ChannelVisibility::Members => {
|
||||
(Some("icons/hash.svg"), Some("Copy private channel link."))
|
||||
}
|
||||
};
|
||||
|
||||
Some(channel.name.as_str())
|
||||
});
|
||||
|
||||
if let Some(name) = channel_name {
|
||||
Cow::Owned(format!("#{}", name))
|
||||
Cow::Owned(format!("{}", name))
|
||||
} else {
|
||||
Cow::Borrowed("Current Call")
|
||||
}
|
||||
@ -1520,28 +1583,30 @@ impl CollabPanel {
|
||||
|
||||
enum AddContact {}
|
||||
let button = match section {
|
||||
Section::ActiveCall => Some(
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme
|
||||
.collab_panel
|
||||
.leave_call_button
|
||||
.style_for(is_selected, state),
|
||||
"icons/exit.svg",
|
||||
"icons/link.svg",
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.with_tooltip::<AddContact>(
|
||||
0,
|
||||
"Leave call",
|
||||
channel_tooltip_text.unwrap(),
|
||||
None,
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
||||
render_icon_button(
|
||||
@ -1626,6 +1691,21 @@ impl CollabPanel {
|
||||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else if let Some(channel_icon) = channel_icon {
|
||||
Some(
|
||||
Svg::new(channel_icon)
|
||||
.with_color(header_style.text.color)
|
||||
.constrained()
|
||||
.with_max_width(icon_size)
|
||||
.with_max_height(icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(icon_size)
|
||||
.contained()
|
||||
.with_margin_right(
|
||||
theme.collab_panel.contact_username.container.margin.left,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
@ -1901,6 +1981,12 @@ impl CollabPanel {
|
||||
let channel_id = channel.id;
|
||||
let collab_theme = &theme.collab_panel;
|
||||
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let other_selected =
|
||||
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
|
||||
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
|
||||
@ -1958,12 +2044,16 @@ impl CollabPanel {
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(
|
||||
Svg::new("icons/hash.svg")
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
Svg::new(if is_public {
|
||||
"icons/public.svg"
|
||||
} else {
|
||||
"icons/hash.svg"
|
||||
})
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child({
|
||||
let style = collab_theme.channel_name.inactive_state();
|
||||
@ -2268,7 +2358,7 @@ impl CollabPanel {
|
||||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
false,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
@ -2301,6 +2391,62 @@ impl CollabPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_chat(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
theme: &theme::CollabPanel,
|
||||
is_selected: bool,
|
||||
ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum ChannelChat {}
|
||||
let host_avatar_width = theme
|
||||
.contact_avatar
|
||||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
|
||||
MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
|
||||
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||
let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||
|
||||
Flex::<Self>::row()
|
||||
.with_child(render_tree_branch(
|
||||
tree_branch,
|
||||
&row.name.text,
|
||||
true,
|
||||
vec2f(host_avatar_width, theme.row_height),
|
||||
cx.font_cache(),
|
||||
))
|
||||
.with_child(
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(theme.channel_hash.width)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("chat", theme.channel_name.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.channel_name.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.channel_row.style_for(is_selected, state))
|
||||
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_channel_invite(
|
||||
channel: Arc<Channel>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
@ -2568,6 +2714,13 @@ impl CollabPanel {
|
||||
},
|
||||
));
|
||||
|
||||
items.push(ContextMenuItem::action(
|
||||
"Copy Channel Link",
|
||||
CopyChannelLink {
|
||||
channel_id: path.channel_id(),
|
||||
},
|
||||
));
|
||||
|
||||
if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
|
||||
let parent_id = path.parent_id();
|
||||
|
||||
@ -2757,6 +2910,9 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
ListEntry::ParticipantScreen { peer_id, .. } => {
|
||||
let Some(peer_id) = peer_id else {
|
||||
return;
|
||||
};
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(*peer_id, cx)
|
||||
@ -3187,49 +3343,19 @@ impl CollabPanel {
|
||||
}
|
||||
|
||||
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let window = cx.window();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if active_call.read_with(&mut cx, |active_call, cx| {
|
||||
if let Some(room) = active_call.room() {
|
||||
let room = room.read(cx);
|
||||
room.is_sharing_project() && room.remote_participants().len() > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
if let Some(mut answer) = answer {
|
||||
if answer.next().await == Some(1) {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let room = active_call
|
||||
.update(&mut cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await?;
|
||||
|
||||
let task = room.update(&mut cx, |room, cx| {
|
||||
let workspace = workspace.upgrade(cx)?;
|
||||
let (project, host) = room.most_active_project()?;
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
Some(workspace::join_remote_project(project, host, app_state, cx))
|
||||
});
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let Some(workspace) = self.workspace.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(handle) = cx.window().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
workspace.read(cx).app_state().clone(),
|
||||
Some(handle),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
|
||||
@ -3246,6 +3372,15 @@ impl CollabPanel {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
|
||||
return;
|
||||
};
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tree_branch(
|
||||
@ -3505,6 +3640,14 @@ impl PartialEq for ListEntry {
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelChat { channel_id } => {
|
||||
if let ListEntry::ChannelChat {
|
||||
channel_id: other_id,
|
||||
} = other
|
||||
{
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelInvite(channel_1) => {
|
||||
if let ListEntry::ChannelInvite(channel_2) = other {
|
||||
return channel_1.id == channel_2.id;
|
||||
|
@ -1,12 +1,16 @@
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::{proto, User, UserId, UserStore};
|
||||
use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
@ -96,11 +100,14 @@ impl ChannelModal {
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let members = channel_store
|
||||
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_mut().members = members);
|
||||
@ -182,6 +189,81 @@ impl View for ChannelModal {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
@ -190,6 +272,7 @@ impl View for ChannelModal {
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(admin.and_then(|admin| {
|
||||
(in_manage && admin).then(|| {
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
}))
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
@ -502,13 +597,13 @@ impl ChannelModalDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.admin),
|
||||
Some(channel_membership.role),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
@ -516,17 +611,21 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
||||
let admin = !admin.unwrap_or(false);
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.admin = admin;
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
@ -572,25 +671,30 @@ impl ChannelModalDelegate {
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, false, cx)
|
||||
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().members.push(ChannelMembership {
|
||||
let new_member = ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
admin: false,
|
||||
});
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
@ -598,7 +702,7 @@ impl ChannelModalDelegate {
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if user_is_admin {
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
|
@ -2,6 +2,7 @@ use crate::{
|
||||
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
|
||||
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
|
||||
};
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
@ -1177,22 +1178,38 @@ impl CollabTitlebarItem {
|
||||
.with_style(theme.titlebar.offline_icon.container)
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.titlebar.outdated_warning.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
"Please update Zed to Collaborate"
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(label, theme.titlebar.outdated_warning.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = { path = "../zed-actions" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
@ -6,8 +6,12 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::cmp::{self, Reverse};
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(toggle_command_palette);
|
||||
@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
)
|
||||
.await
|
||||
};
|
||||
let intercept_result = cx.read(|cx| {
|
||||
let mut intercept_result = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||
if parse_zed_link(&query).is_some() {
|
||||
intercept_result = Some(CommandInterceptResult {
|
||||
action: OpenZedURL { url: query.clone() }.boxed_clone(),
|
||||
string: query.clone(),
|
||||
positions: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
|
@ -38,6 +38,10 @@ impl DiagnosticIndicator {
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
@ -57,7 +57,6 @@ log.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub show_completion_documentation: bool,
|
||||
pub use_on_type_format: bool,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub relative_line_numbers: bool,
|
||||
@ -33,6 +34,7 @@ pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub show_completion_documentation: Option<bool>,
|
||||
pub use_on_type_format: Option<bool>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
pub relative_line_numbers: Option<bool>,
|
||||
|
@ -19,8 +19,8 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||||
Override, Point,
|
||||
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
|
||||
LanguageRegistry, Override, Point,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
@ -1334,7 +1334,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
@ -1345,9 +1345,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"ˇone
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
@ -1367,32 +1380,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
||||
four
|
||||
five
|
||||
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
sixˇ"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
|
||||
cx.assert_editor_state(
|
||||
&r#"one
|
||||
two
|
||||
ˇ
|
||||
three
|
||||
four
|
||||
five
|
||||
ˇ
|
||||
six"#
|
||||
.unindent(),
|
||||
);
|
||||
@ -5090,7 +5077,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
|
||||
});
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
@ -5106,6 +5095,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
// Enable Prettier formatting for the same buffer, and ensure
|
||||
// LSP is called instead of Prettier.
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
@ -5114,7 +5109,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
project.update(cx, |project, _| {
|
||||
project.enable_test_prettier(&[]);
|
||||
project.languages().add(Arc::new(language));
|
||||
});
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.await
|
||||
@ -5232,7 +5230,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
@ -5431,9 +5431,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
additional edit
|
||||
"});
|
||||
cx.simulate_keystroke(" ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.simulate_keystroke("s");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
@ -5495,12 +5495,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.set_state("editorˇ");
|
||||
cx.simulate_keystroke(".");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.simulate_keystroke("c");
|
||||
cx.simulate_keystroke("l");
|
||||
cx.simulate_keystroke("o");
|
||||
cx.assert_editor_state("editor.cloˇ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
});
|
||||
@ -7789,7 +7789,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("-");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
@ -7802,7 +7802,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
@ -7818,7 +7818,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-yellow"]
|
||||
@ -7829,6 +7829,75 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
|
||||
});
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
|
||||
let test_plugin = "test_plugin";
|
||||
let _ = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||
parser_name: Some("test_parser"),
|
||||
plugin_names: vec![test_plugin],
|
||||
}],
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
let prettier_format_suffix = project.update(cx, |project, _| {
|
||||
let suffix = project.enable_test_prettier(&[test_plugin]);
|
||||
project.languages().add(Arc::new(language));
|
||||
suffix
|
||||
});
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_text = "one\ntwo\nthree\n";
|
||||
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
||||
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||
});
|
||||
format.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
buffer_text.to_string() + prettier_format_suffix,
|
||||
"Test prettier formatting was not applied to the original buffer text",
|
||||
);
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||
});
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||
});
|
||||
format.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||||
"Autoformatting (via test prettier) was not applied to the original buffer text",
|
||||
);
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
@ -2372,7 +2372,7 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
editor.context_menu.read().as_ref(),
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
@ -2383,9 +2383,13 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
let mut hover = editor
|
||||
.hover_state
|
||||
.render(&snapshot, &style, visible_rows, cx);
|
||||
let mut hover = editor.hover_state.render(
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
);
|
||||
let mode = editor.mode;
|
||||
|
||||
let mut fold_indicators = editor.render_fold_indicators(
|
||||
|
@ -9,13 +9,15 @@ use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
|
||||
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use language::{
|
||||
markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
|
||||
};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
this.hover_state.diagnostic_popover = None;
|
||||
})?;
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
|
||||
let blocks = vec![inlay_hover.tooltip];
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
blocks: vec![inlay_hover.tooltip],
|
||||
language: None,
|
||||
rendered_content: None,
|
||||
blocks,
|
||||
parsed_content,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@ -288,35 +293,38 @@ fn show_hover(
|
||||
});
|
||||
})?;
|
||||
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.is_empty() {
|
||||
return None;
|
||||
let hover_result = hover_request.await.ok().flatten();
|
||||
let hover_popover = match hover_result {
|
||||
Some(hover_result) if !hover_result.is_empty() => {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.start);
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.end);
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
};
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
blocks,
|
||||
parsed_content,
|
||||
})
|
||||
}
|
||||
|
||||
// Create symbol range of anchors for highlighting and filtering
|
||||
// of future requests.
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.start);
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.end);
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
};
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
})
|
||||
});
|
||||
_ => None,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(symbol_range) = hover_popover
|
||||
@ -345,44 +353,56 @@ fn show_hover(
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> RichText {
|
||||
let mut data = RichText {
|
||||
text: Default::default(),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
};
|
||||
language: Option<Arc<Language>>,
|
||||
) -> markdown::ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
new_paragraph(&mut data.text, &mut Vec::new());
|
||||
data.text.push_str(&block.text);
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
render_markdown_mut(&block.text, language_registry, language, &mut data)
|
||||
markdown::parse_markdown_block(
|
||||
&block.text,
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
|
||||
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
|
||||
} else {
|
||||
data.text.push_str(&block.text);
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.text = data.text.trim().to_string();
|
||||
|
||||
data
|
||||
ParsedMarkdown {
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -403,6 +423,7 @@ impl HoverState {
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<u32>,
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
@ -432,7 +453,7 @@ impl HoverState {
|
||||
elements.push(diagnostic_popover.render(style, cx));
|
||||
}
|
||||
if let Some(info_popover) = self.info_popover.as_mut() {
|
||||
elements.push(info_popover.render(style, cx));
|
||||
elements.push(info_popover.render(style, workspace, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
@ -444,32 +465,23 @@ pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
symbol_range: RangeInEditor,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RichText>,
|
||||
parsed_content: ParsedMarkdown,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
||||
render_blocks(
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
|
||||
let code_span_background_color = style.document_highlight_read_background;
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.scrollable::<HoverBlock>(1, None, cx)
|
||||
.with_child(rendered_content.element(
|
||||
style.syntax.clone(),
|
||||
style.text.clone(),
|
||||
code_span_background_color,
|
||||
.scrollable::<HoverBlock>(0, None, cx)
|
||||
.with_child(crate::render_parsed_markdown::<HoverBlock>(
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
@ -572,7 +584,6 @@ mod tests {
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use rich_text::Highlight;
|
||||
use smol::stream::StreamExt;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
@ -793,7 +804,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
@ -900,7 +911,7 @@ mod tests {
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one [two](the-url) three".to_string(),
|
||||
text: "one [two](https://the-url) three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
@ -921,7 +932,7 @@ mod tests {
|
||||
- a
|
||||
- b
|
||||
* two
|
||||
- [c](the-url)
|
||||
- [c](https://the-url)
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
@ -985,7 +996,7 @@ mod tests {
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
@ -1001,11 +1012,8 @@ mod tests {
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let style = match highlight {
|
||||
Highlight::Id(id) => id.style(&style.syntax)?,
|
||||
Highlight::Highlight(style) => style.clone(),
|
||||
};
|
||||
Some((range.clone(), style))
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -1258,11 +1266,7 @@ mod tests {
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for new type hint")
|
||||
.text,
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for `{new_type_label}`"),
|
||||
"Rendered text should not anyhow alter backticks"
|
||||
);
|
||||
@ -1316,11 +1320,7 @@ mod tests {
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for struct hint")
|
||||
.text,
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
|
@ -263,7 +263,7 @@ pub fn start_of_paragraph(
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == 0 {
|
||||
return map.max_point();
|
||||
return DisplayPoint::zero();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
@ -290,7 +290,7 @@ pub fn end_of_paragraph(
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == map.max_buffer_row() {
|
||||
return DisplayPoint::zero();
|
||||
return map.max_point();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
|
@ -498,77 +498,91 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
for (buffer_id, mut edits) in buffer_edits {
|
||||
edits.sort_unstable_by_key(|edit| edit.range.start);
|
||||
self.buffers.borrow()[&buffer_id]
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let mut edits = edits.into_iter().peekable();
|
||||
let mut insertions = Vec::new();
|
||||
let mut original_indent_columns = Vec::new();
|
||||
let mut deletions = Vec::new();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
while let Some(BufferEdit {
|
||||
mut range,
|
||||
new_text,
|
||||
mut is_insertion,
|
||||
original_indent_column,
|
||||
}) = edits.next()
|
||||
{
|
||||
drop(cursor);
|
||||
drop(snapshot);
|
||||
// Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
|
||||
fn tail(
|
||||
this: &mut MultiBuffer,
|
||||
buffer_edits: HashMap<u64, Vec<BufferEdit>>,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
edited_excerpt_ids: Vec<ExcerptId>,
|
||||
cx: &mut ModelContext<MultiBuffer>,
|
||||
) {
|
||||
for (buffer_id, mut edits) in buffer_edits {
|
||||
edits.sort_unstable_by_key(|edit| edit.range.start);
|
||||
this.buffers.borrow()[&buffer_id]
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let mut edits = edits.into_iter().peekable();
|
||||
let mut insertions = Vec::new();
|
||||
let mut original_indent_columns = Vec::new();
|
||||
let mut deletions = Vec::new();
|
||||
let empty_str: Arc<str> = "".into();
|
||||
while let Some(BufferEdit {
|
||||
range: next_range,
|
||||
is_insertion: next_is_insertion,
|
||||
..
|
||||
}) = edits.peek()
|
||||
mut range,
|
||||
new_text,
|
||||
mut is_insertion,
|
||||
original_indent_column,
|
||||
}) = edits.next()
|
||||
{
|
||||
if range.end >= next_range.start {
|
||||
range.end = cmp::max(next_range.end, range.end);
|
||||
is_insertion |= *next_is_insertion;
|
||||
edits.next();
|
||||
} else {
|
||||
break;
|
||||
while let Some(BufferEdit {
|
||||
range: next_range,
|
||||
is_insertion: next_is_insertion,
|
||||
..
|
||||
}) = edits.peek()
|
||||
{
|
||||
if range.end >= next_range.start {
|
||||
range.end = cmp::max(next_range.end, range.end);
|
||||
is_insertion |= *next_is_insertion;
|
||||
edits.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if is_insertion {
|
||||
original_indent_columns.push(original_indent_column);
|
||||
insertions.push((
|
||||
buffer.anchor_before(range.start)
|
||||
..buffer.anchor_before(range.end),
|
||||
new_text.clone(),
|
||||
));
|
||||
} else if !range.is_empty() {
|
||||
deletions.push((
|
||||
buffer.anchor_before(range.start)
|
||||
..buffer.anchor_before(range.end),
|
||||
empty_str.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if is_insertion {
|
||||
original_indent_columns.push(original_indent_column);
|
||||
insertions.push((
|
||||
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
|
||||
new_text.clone(),
|
||||
));
|
||||
} else if !range.is_empty() {
|
||||
deletions.push((
|
||||
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
|
||||
empty_str.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let deletion_autoindent_mode =
|
||||
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: Default::default(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let insertion_autoindent_mode =
|
||||
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let deletion_autoindent_mode =
|
||||
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns: Default::default(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let insertion_autoindent_mode =
|
||||
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
|
||||
Some(AutoindentMode::Block {
|
||||
original_indent_columns,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
buffer.edit(deletions, deletion_autoindent_mode, cx);
|
||||
buffer.edit(insertions, insertion_autoindent_mode, cx);
|
||||
})
|
||||
}
|
||||
|
||||
buffer.edit(deletions, deletion_autoindent_mode, cx);
|
||||
buffer.edit(insertions, insertion_autoindent_mode, cx);
|
||||
})
|
||||
cx.emit(Event::ExcerptsEdited {
|
||||
ids: edited_excerpt_ids,
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(Event::ExcerptsEdited {
|
||||
ids: edited_excerpt_ids,
|
||||
});
|
||||
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
|
||||
}
|
||||
|
||||
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
|
@ -222,6 +222,10 @@ fn toggle_or_cycle_file_finder(
|
||||
.as_ref()
|
||||
.and_then(|found_path| found_path.absolute.as_ref())
|
||||
})
|
||||
.filter(|(_, history_abs_path)| match history_abs_path {
|
||||
Some(abs_path) => history_file_exists(abs_path),
|
||||
None => true,
|
||||
})
|
||||
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
|
||||
)
|
||||
.collect();
|
||||
@ -246,6 +250,16 @@ fn toggle_or_cycle_file_finder(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||
abs_path.exists()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn history_file_exists(abs_path: &PathBuf) -> bool {
|
||||
!abs_path.ends_with("nonexistent.rs")
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Selected(ProjectPath),
|
||||
Dismissed,
|
||||
@ -515,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
project
|
||||
.worktree_for_id(history_item.project.worktree_id, cx)
|
||||
.is_some()
|
||||
|| (project.is_local()
|
||||
&& history_item
|
||||
.absolute
|
||||
.as_ref()
|
||||
.filter(|abs_path| abs_path.exists())
|
||||
.is_some())
|
||||
|| (project.is_local() && history_item.absolute.is_some())
|
||||
})
|
||||
.cloned()
|
||||
.map(|p| (p, None))
|
||||
@ -1900,13 +1909,8 @@ mod tests {
|
||||
.matches
|
||||
.search
|
||||
.iter()
|
||||
.map(|e| e.path.to_path_buf())
|
||||
.map(|path_match| path_match.path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
search_entries.len(),
|
||||
4,
|
||||
"All history and the new file should be found after query {query} as search results"
|
||||
);
|
||||
assert_eq!(
|
||||
search_entries,
|
||||
vec![
|
||||
@ -1920,6 +1924,100 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nonexistent_history_items_not_shown(
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"nonexistent.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let workspace = window.root(cx);
|
||||
// generate some history to select from
|
||||
open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"non",
|
||||
1,
|
||||
"nonexistent.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"thi",
|
||||
1,
|
||||
"third.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
open_close_queried_buffer(
|
||||
"fir",
|
||||
1,
|
||||
"first.rs",
|
||||
window.into(),
|
||||
&workspace,
|
||||
&deterministic,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(window.into(), Toggle);
|
||||
let query = "rs";
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
finder
|
||||
.update(cx, |finder, cx| {
|
||||
finder.delegate_mut().update_matches(query.to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
finder.read_with(cx, |finder, _| {
|
||||
let delegate = finder.delegate();
|
||||
let history_entries = delegate
|
||||
.matches
|
||||
.history
|
||||
.iter()
|
||||
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
history_entries,
|
||||
vec![
|
||||
PathBuf::from("test/first.rs"),
|
||||
PathBuf::from("test/third.rs"),
|
||||
],
|
||||
"Should have all opened files in the history, except the ones that do not exist on disk"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async fn open_close_queried_buffer(
|
||||
input: &str,
|
||||
expected_matches: usize,
|
||||
|
@ -85,7 +85,7 @@ pub struct RemoveOptions {
|
||||
pub ignore_if_not_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub inode: u64,
|
||||
pub mtime: SystemTime,
|
||||
|
@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
json::{self, ToJson, Value},
|
||||
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
|
||||
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
@ -10,10 +11,10 @@ use pathfinder_geometry::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollState {
|
||||
scroll_to: Cell<Option<usize>>,
|
||||
scroll_position: Cell<f32>,
|
||||
type_tag: TypeTag,
|
||||
}
|
||||
|
||||
pub struct Flex<V> {
|
||||
@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
|
||||
where
|
||||
Tag: 'static,
|
||||
{
|
||||
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
|
||||
scroll_state.read(cx).scroll_to.set(scroll_to);
|
||||
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
|
||||
element_id,
|
||||
Rc::new(ScrollState {
|
||||
scroll_to: Cell::new(scroll_to),
|
||||
scroll_position: Default::default(),
|
||||
type_tag: TypeTag::new::<Tag>(),
|
||||
}),
|
||||
);
|
||||
self.scroll_state = Some((scroll_state, cx.handle().id()));
|
||||
self
|
||||
}
|
||||
@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
|
||||
if let Some((scroll_state, id)) = &self.scroll_state {
|
||||
let scroll_state = scroll_state.read(cx).clone();
|
||||
cx.scene().push_mouse_region(
|
||||
crate::MouseRegion::new::<Self>(*id, 0, bounds)
|
||||
.on_scroll({
|
||||
let axis = self.axis;
|
||||
move |e, _: &mut V, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
crate::MouseRegion::from_handlers(
|
||||
scroll_state.type_tag,
|
||||
*id,
|
||||
0,
|
||||
bounds,
|
||||
Default::default(),
|
||||
)
|
||||
.on_scroll({
|
||||
let axis = self.axis;
|
||||
move |e, _: &mut V, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
scroll_delta.y()
|
||||
}
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
scroll_delta.y()
|
||||
}
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(scroll_state.scroll_position.get() - delta);
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(scroll_state.scroll_position.get() - delta);
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
})
|
||||
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
|
||||
}
|
||||
})
|
||||
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -140,6 +140,10 @@ unsafe fn build_classes() {
|
||||
sel!(application:openURLs:),
|
||||
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(application:continueUserActivity:restorationHandler:),
|
||||
continue_user_activity as extern "C" fn(&mut Object, Sel, id, id, id),
|
||||
);
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
@ -1009,6 +1013,26 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) {
|
||||
let url = unsafe {
|
||||
let url: id = msg_send!(user_activity, webpageURL);
|
||||
if url == nil {
|
||||
log::error!("got unexpected user activity");
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
CStr::from_ptr(url.absoluteString().UTF8String())
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let platform = unsafe { get_foreground_platform(this) };
|
||||
if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
|
||||
callback(url.into_iter().collect());
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
|
||||
unsafe {
|
||||
let platform = get_foreground_platform(this);
|
||||
|
@ -45,6 +45,7 @@ lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -1,11 +1,13 @@
|
||||
pub use crate::{
|
||||
diagnostic_set::DiagnosticSet,
|
||||
highlight_map::{HighlightId, HighlightMap},
|
||||
markdown::ParsedMarkdown,
|
||||
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
|
||||
};
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
markdown::parse_markdown,
|
||||
outline::OutlineItem,
|
||||
syntax_map::{
|
||||
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||
@ -143,11 +145,51 @@ pub struct Diagnostic {
|
||||
pub is_unnecessary: bool,
|
||||
}
|
||||
|
||||
pub async fn prepare_completion_documentation(
|
||||
documentation: &lsp::Documentation,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> Documentation {
|
||||
match documentation {
|
||||
lsp::Documentation::String(text) => {
|
||||
if text.lines().count() <= 1 {
|
||||
Documentation::SingleLine(text.clone())
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
|
||||
lsp::MarkupKind::PlainText => {
|
||||
if value.lines().count() <= 1 {
|
||||
Documentation::SingleLine(value.clone())
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
lsp::MarkupKind::Markdown => {
|
||||
let parsed = parse_markdown(value, language_registry, language).await;
|
||||
Documentation::MultiLineMarkdown(parsed)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Documentation {
|
||||
Undocumented,
|
||||
SingleLine(String),
|
||||
MultiLinePlainText(String),
|
||||
MultiLineMarkdown(ParsedMarkdown),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Completion {
|
||||
pub old_range: Range<Anchor>,
|
||||
pub new_text: String,
|
||||
pub label: CodeLabel,
|
||||
pub documentation: Option<Documentation>,
|
||||
pub server_id: LanguageServerId,
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
}
|
||||
@ -1406,82 +1448,95 @@ impl Buffer {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.start_transaction();
|
||||
self.pending_autoindent.take();
|
||||
let autoindent_request = autoindent_mode
|
||||
.and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
|
||||
// Non-generic part hoisted out to reduce LLVM IR size.
|
||||
fn tail(
|
||||
this: &mut Buffer,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
cx: &mut ModelContext<Buffer>,
|
||||
) -> Option<clock::Lamport> {
|
||||
this.start_transaction();
|
||||
this.pending_autoindent.take();
|
||||
let autoindent_request = autoindent_mode
|
||||
.and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode)));
|
||||
|
||||
let edit_operation = self.text.edit(edits.iter().cloned());
|
||||
let edit_id = edit_operation.timestamp();
|
||||
let edit_operation = this.text.edit(edits.iter().cloned());
|
||||
let edit_id = edit_operation.timestamp();
|
||||
|
||||
if let Some((before_edit, mode)) = autoindent_request {
|
||||
let mut delta = 0isize;
|
||||
let entries = edits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.zip(&edit_operation.as_edit().unwrap().new_text)
|
||||
.map(|((ix, (range, _)), new_text)| {
|
||||
let new_text_length = new_text.len();
|
||||
let old_start = range.start.to_point(&before_edit);
|
||||
let new_start = (delta + range.start as isize) as usize;
|
||||
delta += new_text_length as isize - (range.end as isize - range.start as isize);
|
||||
if let Some((before_edit, mode)) = autoindent_request {
|
||||
let mut delta = 0isize;
|
||||
let entries = edits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.zip(&edit_operation.as_edit().unwrap().new_text)
|
||||
.map(|((ix, (range, _)), new_text)| {
|
||||
let new_text_length = new_text.len();
|
||||
let old_start = range.start.to_point(&before_edit);
|
||||
let new_start = (delta + range.start as isize) as usize;
|
||||
delta +=
|
||||
new_text_length as isize - (range.end as isize - range.start as isize);
|
||||
|
||||
let mut range_of_insertion_to_indent = 0..new_text_length;
|
||||
let mut first_line_is_new = false;
|
||||
let mut original_indent_column = None;
|
||||
let mut range_of_insertion_to_indent = 0..new_text_length;
|
||||
let mut first_line_is_new = false;
|
||||
let mut original_indent_column = None;
|
||||
|
||||
// When inserting an entire line at the beginning of an existing line,
|
||||
// treat the insertion as new.
|
||||
if new_text.contains('\n')
|
||||
&& old_start.column <= before_edit.indent_size_for_line(old_start.row).len
|
||||
{
|
||||
first_line_is_new = true;
|
||||
}
|
||||
|
||||
// When inserting text starting with a newline, avoid auto-indenting the
|
||||
// previous line.
|
||||
if new_text.starts_with('\n') {
|
||||
range_of_insertion_to_indent.start += 1;
|
||||
first_line_is_new = true;
|
||||
}
|
||||
|
||||
// Avoid auto-indenting after the insertion.
|
||||
if let AutoindentMode::Block {
|
||||
original_indent_columns,
|
||||
} = &mode
|
||||
{
|
||||
original_indent_column =
|
||||
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
|
||||
indent_size_for_text(
|
||||
new_text[range_of_insertion_to_indent.clone()].chars(),
|
||||
)
|
||||
.len
|
||||
}));
|
||||
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
|
||||
range_of_insertion_to_indent.end -= 1;
|
||||
// When inserting an entire line at the beginning of an existing line,
|
||||
// treat the insertion as new.
|
||||
if new_text.contains('\n')
|
||||
&& old_start.column
|
||||
<= before_edit.indent_size_for_line(old_start.row).len
|
||||
{
|
||||
first_line_is_new = true;
|
||||
}
|
||||
}
|
||||
|
||||
AutoindentRequestEntry {
|
||||
first_line_is_new,
|
||||
original_indent_column,
|
||||
indent_size: before_edit.language_indent_size_at(range.start, cx),
|
||||
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
|
||||
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// When inserting text starting with a newline, avoid auto-indenting the
|
||||
// previous line.
|
||||
if new_text.starts_with('\n') {
|
||||
range_of_insertion_to_indent.start += 1;
|
||||
first_line_is_new = true;
|
||||
}
|
||||
|
||||
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||
before_edit,
|
||||
entries,
|
||||
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
|
||||
}));
|
||||
// Avoid auto-indenting after the insertion.
|
||||
if let AutoindentMode::Block {
|
||||
original_indent_columns,
|
||||
} = &mode
|
||||
{
|
||||
original_indent_column = Some(
|
||||
original_indent_columns.get(ix).copied().unwrap_or_else(|| {
|
||||
indent_size_for_text(
|
||||
new_text[range_of_insertion_to_indent.clone()].chars(),
|
||||
)
|
||||
.len
|
||||
}),
|
||||
);
|
||||
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
|
||||
range_of_insertion_to_indent.end -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
AutoindentRequestEntry {
|
||||
first_line_is_new,
|
||||
original_indent_column,
|
||||
indent_size: before_edit.language_indent_size_at(range.start, cx),
|
||||
range: this
|
||||
.anchor_before(new_start + range_of_insertion_to_indent.start)
|
||||
..this.anchor_after(new_start + range_of_insertion_to_indent.end),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||
before_edit,
|
||||
entries,
|
||||
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
|
||||
}));
|
||||
}
|
||||
|
||||
this.end_transaction(cx);
|
||||
this.send_operation(Operation::Buffer(edit_operation), cx);
|
||||
Some(edit_id)
|
||||
}
|
||||
|
||||
self.end_transaction(cx);
|
||||
self.send_operation(Operation::Buffer(edit_operation), cx);
|
||||
Some(edit_id)
|
||||
tail(self, edits, autoindent_mode, cx)
|
||||
}
|
||||
|
||||
fn did_edit(
|
||||
|
@ -2,6 +2,7 @@ mod buffer;
|
||||
mod diagnostic_set;
|
||||
mod highlight_map;
|
||||
pub mod language_settings;
|
||||
pub mod markdown;
|
||||
mod outline;
|
||||
pub mod proto;
|
||||
mod syntax_map;
|
||||
@ -110,7 +111,6 @@ pub struct LanguageServerName(pub Arc<str>);
|
||||
pub struct CachedLspAdapter {
|
||||
pub name: LanguageServerName,
|
||||
pub short_name: &'static str,
|
||||
pub initialization_options: Option<Value>,
|
||||
pub disk_based_diagnostic_sources: Vec<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub language_ids: HashMap<String, String>,
|
||||
@ -121,7 +121,6 @@ impl CachedLspAdapter {
|
||||
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
|
||||
let name = adapter.name().await;
|
||||
let short_name = adapter.short_name();
|
||||
let initialization_options = adapter.initialization_options().await;
|
||||
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
|
||||
let disk_based_diagnostics_progress_token =
|
||||
adapter.disk_based_diagnostics_progress_token().await;
|
||||
@ -130,7 +129,6 @@ impl CachedLspAdapter {
|
||||
Arc::new(CachedLspAdapter {
|
||||
name,
|
||||
short_name,
|
||||
initialization_options,
|
||||
disk_based_diagnostic_sources,
|
||||
disk_based_diagnostics_progress_token,
|
||||
language_ids,
|
||||
@ -227,6 +225,10 @@ impl CachedLspAdapter {
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter.label_for_symbol(name, kind, language).await
|
||||
}
|
||||
|
||||
pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
self.adapter.enabled_formatters()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LspAdapterDelegate: Send + Sync {
|
||||
@ -333,6 +335,33 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum BundledFormatter {
|
||||
Prettier {
|
||||
// See https://prettier.io/docs/en/options.html#parser for a list of valid values.
|
||||
// Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
|
||||
// There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
|
||||
//
|
||||
// But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
|
||||
// For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
|
||||
parser_name: Option<&'static str>,
|
||||
plugin_names: Vec<&'static str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl BundledFormatter {
|
||||
pub fn prettier(parser_name: &'static str) -> Self {
|
||||
Self::Prettier {
|
||||
parser_name: Some(parser_name),
|
||||
plugin_names: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@ -467,6 +496,7 @@ pub struct FakeLspAdapter {
|
||||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub disk_based_diagnostics_sources: Vec<String>,
|
||||
pub enabled_formatters: Vec<BundledFormatter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@ -1729,6 +1759,7 @@ impl Default for FakeLspAdapter {
|
||||
disk_based_diagnostics_progress_token: None,
|
||||
initialization_options: None,
|
||||
disk_based_diagnostics_sources: Vec::new(),
|
||||
enabled_formatters: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1785,6 +1816,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
||||
async fn initialization_options(&self) -> Option<Value> {
|
||||
self.initialization_options.clone()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
self.enabled_formatters.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
|
||||
|
@ -50,6 +50,7 @@ pub struct LanguageSettings {
|
||||
pub remove_trailing_whitespace_on_save: bool,
|
||||
pub ensure_final_newline_on_save: bool,
|
||||
pub formatter: Formatter,
|
||||
pub prettier: HashMap<String, serde_json::Value>,
|
||||
pub enable_language_server: bool,
|
||||
pub show_copilot_suggestions: bool,
|
||||
pub show_whitespaces: ShowWhitespaceSetting,
|
||||
@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
|
||||
#[serde(default)]
|
||||
pub formatter: Option<Formatter>,
|
||||
#[serde(default)]
|
||||
pub prettier: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
pub enable_language_server: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_copilot_suggestions: Option<bool>,
|
||||
@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting {
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Formatter {
|
||||
#[default]
|
||||
Auto,
|
||||
LanguageServer,
|
||||
Prettier,
|
||||
External {
|
||||
command: Arc<str>,
|
||||
arguments: Arc<[String]>,
|
||||
@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
src.preferred_line_length,
|
||||
);
|
||||
merge(&mut settings.formatter, src.formatter.clone());
|
||||
merge(&mut settings.prettier, src.prettier.clone());
|
||||
merge(&mut settings.format_on_save, src.format_on_save.clone());
|
||||
merge(
|
||||
&mut settings.remove_trailing_whitespace_on_save,
|
||||
|
301
crates/language/src/markdown.rs
Normal file
301
crates/language/src/markdown.rs
Normal file
@ -0,0 +1,301 @@
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use crate::{HighlightId, Language, LanguageRegistry};
|
||||
use gpui::fonts::{self, HighlightStyle, Weight};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedMarkdown {
|
||||
pub text: String,
|
||||
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
pub region_ranges: Vec<Range<usize>>,
|
||||
pub regions: Vec<ParsedRegion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MarkdownHighlight {
|
||||
Style(MarkdownHighlightStyle),
|
||||
Code(HighlightId),
|
||||
}
|
||||
|
||||
impl MarkdownHighlight {
|
||||
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
|
||||
match self {
|
||||
MarkdownHighlight::Style(style) => {
|
||||
let mut highlight = HighlightStyle::default();
|
||||
|
||||
if style.italic {
|
||||
highlight.italic = Some(true);
|
||||
}
|
||||
|
||||
if style.underline {
|
||||
highlight.underline = Some(fonts::Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style.weight != fonts::Weight::default() {
|
||||
highlight.weight = Some(style.weight);
|
||||
}
|
||||
|
||||
Some(highlight)
|
||||
}
|
||||
|
||||
MarkdownHighlight::Code(id) => id.style(theme),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct MarkdownHighlightStyle {
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub weight: Weight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedRegion {
|
||||
pub code: bool,
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Link {
|
||||
Web { url: String },
|
||||
Path { path: PathBuf },
|
||||
}
|
||||
|
||||
impl Link {
|
||||
fn identify(text: String) -> Option<Link> {
|
||||
if text.starts_with("http") {
|
||||
return Some(Link::Web { url: text });
|
||||
}
|
||||
|
||||
let path = PathBuf::from(text);
|
||||
if path.is_absolute() {
|
||||
return Some(Link::Path { path });
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_markdown(
|
||||
markdown: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
|
||||
parse_markdown_block(
|
||||
markdown,
|
||||
language_registry,
|
||||
language,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await;
|
||||
|
||||
ParsedMarkdown {
|
||||
text,
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_markdown_block(
|
||||
markdown: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
region_ranges: &mut Vec<Range<usize>>,
|
||||
regions: &mut Vec<ParsedRegion>,
|
||||
) {
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut link_url = None;
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(&markdown, Options::all()) {
|
||||
let prev_len = text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
if let Some(language) = ¤t_language {
|
||||
highlight_code(text, highlights, t.as_ref(), language);
|
||||
} else {
|
||||
text.push_str(t.as_ref());
|
||||
|
||||
let mut style = MarkdownHighlightStyle::default();
|
||||
|
||||
if bold_depth > 0 {
|
||||
style.weight = Weight::BOLD;
|
||||
}
|
||||
|
||||
if italic_depth > 0 {
|
||||
style.italic = true;
|
||||
}
|
||||
|
||||
if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
|
||||
region_ranges.push(prev_len..text.len());
|
||||
regions.push(ParsedRegion {
|
||||
code: false,
|
||||
link: Some(link),
|
||||
});
|
||||
style.underline = true;
|
||||
}
|
||||
|
||||
if style != MarkdownHighlightStyle::default() {
|
||||
let mut new_highlight = true;
|
||||
if let Some((last_range, MarkdownHighlight::Style(last_style))) =
|
||||
highlights.last_mut()
|
||||
{
|
||||
if last_range.end == prev_len && last_style == &style {
|
||||
last_range.end = text.len();
|
||||
new_highlight = false;
|
||||
}
|
||||
}
|
||||
if new_highlight {
|
||||
let range = prev_len..text.len();
|
||||
highlights.push((range, MarkdownHighlight::Style(style)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
region_ranges.push(prev_len..text.len());
|
||||
|
||||
let link = link_url.clone().and_then(|u| Link::identify(u));
|
||||
if link.is_some() {
|
||||
highlights.push((
|
||||
prev_len..text.len(),
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
underline: true,
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
regions.push(ParsedRegion { code: true, link });
|
||||
}
|
||||
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||
|
||||
Tag::Heading(_, _, _) => {
|
||||
new_paragraph(text, &mut list_stack);
|
||||
bold_depth += 1;
|
||||
}
|
||||
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(text, &mut list_stack);
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
language.clone()
|
||||
}
|
||||
}
|
||||
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
|
||||
Tag::Strong => bold_depth += 1,
|
||||
|
||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||
|
||||
Tag::List(number) => {
|
||||
list_stack.push((number, false));
|
||||
}
|
||||
|
||||
Tag::Item => {
|
||||
let len = list_stack.len();
|
||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||
*has_content = false;
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..len - 1 {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if let Some(number) = list_number {
|
||||
text.push_str(&format!("{}. ", number));
|
||||
*number += 1;
|
||||
*has_content = false;
|
||||
} else {
|
||||
text.push_str("- ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||
Tag::CodeBlock(_) => current_language = None,
|
||||
Tag::Emphasis => italic_depth -= 1,
|
||||
Tag::Strong => bold_depth -= 1,
|
||||
Tag::Link(_, _, _) => link_url = None,
|
||||
Tag::List(_) => drop(list_stack.pop()),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::HardBreak => text.push('\n'),
|
||||
|
||||
Event::SoftBreak => text.push(' '),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
content: &str,
|
||||
language: &Arc<Language>,
|
||||
) {
|
||||
let prev_len = text.len();
|
||||
text.push_str(content);
|
||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||
let highlight = MarkdownHighlight::Code(highlight_id);
|
||||
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||
let mut is_subsequent_paragraph_of_list = false;
|
||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||
if *has_content {
|
||||
is_subsequent_paragraph_of_list = true;
|
||||
} else {
|
||||
*has_content = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if is_subsequent_paragraph_of_list {
|
||||
text.push_str(" ");
|
||||
}
|
||||
}
|
@ -482,6 +482,7 @@ pub async fn deserialize_completion(
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(completion.server_id as usize),
|
||||
lsp_completion,
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{Editor, MoveToEnd};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
@ -11,7 +11,7 @@ use gpui::{
|
||||
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageServerId, LanguageServerName};
|
||||
use language::{LanguageServerId, LanguageServerName};
|
||||
use lsp::IoKind;
|
||||
use project::{search::SearchQuery, Project};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
@ -22,8 +22,9 @@ use workspace::{
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
|
||||
};
|
||||
|
||||
const SEND_LINE: &str = "// Send:\n";
|
||||
const RECEIVE_LINE: &str = "// Receive:\n";
|
||||
const SEND_LINE: &str = "// Send:";
|
||||
const RECEIVE_LINE: &str = "// Receive:";
|
||||
const MAX_STORED_LOG_ENTRIES: usize = 2000;
|
||||
|
||||
pub struct LogStore {
|
||||
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
||||
@ -36,24 +37,25 @@ struct ProjectState {
|
||||
}
|
||||
|
||||
struct LanguageServerState {
|
||||
log_buffer: ModelHandle<Buffer>,
|
||||
log_messages: VecDeque<String>,
|
||||
rpc_state: Option<LanguageServerRpcState>,
|
||||
_io_logs_subscription: Option<lsp::Subscription>,
|
||||
_lsp_logs_subscription: Option<lsp::Subscription>,
|
||||
}
|
||||
|
||||
struct LanguageServerRpcState {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
rpc_messages: VecDeque<String>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
}
|
||||
|
||||
pub struct LspLogView {
|
||||
pub(crate) editor: ViewHandle<Editor>,
|
||||
editor_subscription: Subscription,
|
||||
log_store: ModelHandle<LogStore>,
|
||||
current_server_id: Option<LanguageServerId>,
|
||||
is_showing_rpc_trace: bool,
|
||||
project: ModelHandle<Project>,
|
||||
_log_store_subscription: Subscription,
|
||||
_log_store_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
pub struct LspLogToolbarItemView {
|
||||
@ -122,10 +124,9 @@ impl LogStore {
|
||||
io_tx,
|
||||
};
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
|
||||
while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
message.push('\n');
|
||||
this.on_io(project, server_id, io_kind, &message, cx);
|
||||
});
|
||||
}
|
||||
@ -168,15 +169,13 @@ impl LogStore {
|
||||
project: &ModelHandle<Project>,
|
||||
id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
) -> Option<&mut LanguageServerState> {
|
||||
let project_state = self.projects.get_mut(&project.downgrade())?;
|
||||
let server_state = project_state.servers.entry(id).or_insert_with(|| {
|
||||
cx.notify();
|
||||
LanguageServerState {
|
||||
rpc_state: None,
|
||||
log_buffer: cx
|
||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
|
||||
.clone(),
|
||||
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
|
||||
_io_logs_subscription: None,
|
||||
_lsp_logs_subscription: None,
|
||||
}
|
||||
@ -186,7 +185,7 @@ impl LogStore {
|
||||
if let Some(server) = server.as_deref() {
|
||||
if server.has_notification_handler::<lsp::notification::LogMessage>() {
|
||||
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
|
||||
return Some(server_state.log_buffer.clone());
|
||||
return Some(server_state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,7 +214,7 @@ impl LogStore {
|
||||
}
|
||||
})
|
||||
});
|
||||
Some(server_state.log_buffer.clone())
|
||||
Some(server_state)
|
||||
}
|
||||
|
||||
fn add_language_server_log(
|
||||
@ -225,24 +224,26 @@ impl LogStore {
|
||||
message: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let buffer = match self
|
||||
let language_server_state = match self
|
||||
.projects
|
||||
.get_mut(&project.downgrade())?
|
||||
.servers
|
||||
.get(&id)
|
||||
.map(|state| state.log_buffer.clone())
|
||||
.get_mut(&id)
|
||||
{
|
||||
Some(existing_buffer) => existing_buffer,
|
||||
Some(existing_state) => existing_state,
|
||||
None => self.add_language_server(&project, id, cx)?,
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let len = buffer.len();
|
||||
let has_newline = message.ends_with("\n");
|
||||
buffer.edit([(len..len, message)], None, cx);
|
||||
if !has_newline {
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, "\n")], None, cx);
|
||||
}
|
||||
|
||||
let log_lines = &mut language_server_state.log_messages;
|
||||
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
|
||||
log_lines.pop_front();
|
||||
}
|
||||
let message = message.trim();
|
||||
log_lines.push_back(message.to_string());
|
||||
cx.emit(Event::NewServerLogEntry {
|
||||
id,
|
||||
entry: message.to_string(),
|
||||
is_rpc: false,
|
||||
});
|
||||
cx.notify();
|
||||
Some(())
|
||||
@ -260,46 +261,32 @@ impl LogStore {
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn log_buffer_for_server(
|
||||
fn server_logs(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
) -> Option<&VecDeque<String>> {
|
||||
let weak_project = project.downgrade();
|
||||
let project_state = self.projects.get(&weak_project)?;
|
||||
let server_state = project_state.servers.get(&server_id)?;
|
||||
Some(server_state.log_buffer.clone())
|
||||
Some(&server_state.log_messages)
|
||||
}
|
||||
|
||||
fn enable_rpc_trace_for_language_server(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
server_id: LanguageServerId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<ModelHandle<Buffer>> {
|
||||
) -> Option<&mut LanguageServerRpcState> {
|
||||
let weak_project = project.downgrade();
|
||||
let project_state = self.projects.get_mut(&weak_project)?;
|
||||
let server_state = project_state.servers.get_mut(&server_id)?;
|
||||
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
|
||||
let language = project.read(cx).languages().language_for_name("JSON");
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
||||
cx.spawn_weak({
|
||||
let buffer = buffer.clone();
|
||||
|_, mut cx| async move {
|
||||
let language = language.await.ok();
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
LanguageServerRpcState {
|
||||
buffer,
|
||||
let rpc_state = server_state
|
||||
.rpc_state
|
||||
.get_or_insert_with(|| LanguageServerRpcState {
|
||||
rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
|
||||
last_message_kind: None,
|
||||
}
|
||||
});
|
||||
Some(rpc_state.buffer.clone())
|
||||
});
|
||||
Some(rpc_state)
|
||||
}
|
||||
|
||||
pub fn disable_rpc_trace_for_language_server(
|
||||
@ -328,7 +315,7 @@ impl LogStore {
|
||||
IoKind::StdIn => false,
|
||||
IoKind::StdErr => {
|
||||
let project = project.upgrade(cx)?;
|
||||
let message = format!("stderr: {}\n", message.trim());
|
||||
let message = format!("stderr: {}", message.trim());
|
||||
self.add_language_server_log(&project, language_server_id, &message, cx);
|
||||
return Some(());
|
||||
}
|
||||
@ -341,24 +328,37 @@ impl LogStore {
|
||||
.get_mut(&language_server_id)?
|
||||
.rpc_state
|
||||
.as_mut()?;
|
||||
state.buffer.update(cx, |buffer, cx| {
|
||||
let kind = if is_received {
|
||||
MessageKind::Receive
|
||||
} else {
|
||||
MessageKind::Send
|
||||
let kind = if is_received {
|
||||
MessageKind::Receive
|
||||
} else {
|
||||
MessageKind::Send
|
||||
};
|
||||
|
||||
let rpc_log_lines = &mut state.rpc_messages;
|
||||
if state.last_message_kind != Some(kind) {
|
||||
let line_before_message = match kind {
|
||||
MessageKind::Send => SEND_LINE,
|
||||
MessageKind::Receive => RECEIVE_LINE,
|
||||
};
|
||||
if state.last_message_kind != Some(kind) {
|
||||
let len = buffer.len();
|
||||
let line = match kind {
|
||||
MessageKind::Send => SEND_LINE,
|
||||
MessageKind::Receive => RECEIVE_LINE,
|
||||
};
|
||||
buffer.edit([(len..len, line)], None, cx);
|
||||
state.last_message_kind = Some(kind);
|
||||
}
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, message)], None, cx);
|
||||
rpc_log_lines.push_back(line_before_message.to_string());
|
||||
cx.emit(Event::NewServerLogEntry {
|
||||
id: language_server_id,
|
||||
entry: line_before_message.to_string(),
|
||||
is_rpc: true,
|
||||
});
|
||||
}
|
||||
|
||||
while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
|
||||
rpc_log_lines.pop_front();
|
||||
}
|
||||
let message = message.trim();
|
||||
rpc_log_lines.push_back(message.to_string());
|
||||
cx.emit(Event::NewServerLogEntry {
|
||||
id: language_server_id,
|
||||
entry: message.to_string(),
|
||||
is_rpc: true,
|
||||
});
|
||||
cx.notify();
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
@ -374,8 +374,7 @@ impl LspLogView {
|
||||
.projects
|
||||
.get(&project.downgrade())
|
||||
.and_then(|project| project.servers.keys().copied().next());
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
||||
let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
|
||||
let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
|
||||
(|| -> Option<()> {
|
||||
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
|
||||
if let Some(current_lsp) = this.current_server_id {
|
||||
@ -411,13 +410,31 @@ impl LspLogView {
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
|
||||
Event::NewServerLogEntry { id, entry, is_rpc } => {
|
||||
if log_view.current_server_id == Some(*id) {
|
||||
if (*is_rpc && log_view.is_showing_rpc_trace)
|
||||
|| (!*is_rpc && !log_view.is_showing_rpc_trace)
|
||||
{
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
editor.handle_input(entry.trim(), cx);
|
||||
editor.handle_input("\n", cx);
|
||||
editor.set_read_only(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
|
||||
let mut this = Self {
|
||||
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
|
||||
editor,
|
||||
editor_subscription,
|
||||
project,
|
||||
log_store,
|
||||
current_server_id: None,
|
||||
is_showing_rpc_trace: false,
|
||||
_log_store_subscription,
|
||||
_log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
this.show_logs_for_server(server_id, cx);
|
||||
@ -425,20 +442,19 @@ impl LspLogView {
|
||||
this
|
||||
}
|
||||
|
||||
fn editor_for_buffer(
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
fn editor_for_logs(
|
||||
log_contents: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ViewHandle<Editor> {
|
||||
) -> (ViewHandle<Editor>, Subscription) {
|
||||
let editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
let mut editor = Editor::multi_line(None, cx);
|
||||
editor.set_text(log_contents, cx);
|
||||
editor.move_to_end(&MoveToEnd, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.move_to_end(&Default::default(), cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
|
||||
.detach();
|
||||
editor
|
||||
let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
|
||||
(editor, editor_subscription)
|
||||
}
|
||||
|
||||
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
||||
@ -487,14 +503,17 @@ impl LspLogView {
|
||||
}
|
||||
|
||||
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||
let buffer = self
|
||||
let log_contents = self
|
||||
.log_store
|
||||
.read(cx)
|
||||
.log_buffer_for_server(&self.project, server_id);
|
||||
if let Some(buffer) = buffer {
|
||||
.server_logs(&self.project, server_id)
|
||||
.map(log_contents);
|
||||
if let Some(log_contents) = log_contents {
|
||||
self.current_server_id = Some(server_id);
|
||||
self.is_showing_rpc_trace = false;
|
||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
||||
let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
|
||||
self.editor = editor;
|
||||
self.editor_subscription = editor_subscription;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@ -504,13 +523,37 @@ impl LspLogView {
|
||||
server_id: LanguageServerId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.log_store.update(cx, |log_set, cx| {
|
||||
log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.enable_rpc_trace_for_language_server(&self.project, server_id)
|
||||
.map(|state| log_contents(&state.rpc_messages))
|
||||
});
|
||||
if let Some(buffer) = buffer {
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_server_id = Some(server_id);
|
||||
self.is_showing_rpc_trace = true;
|
||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
||||
let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("log buffer should be a singleton")
|
||||
.update(cx, |_, cx| {
|
||||
cx.spawn_weak({
|
||||
let buffer = cx.handle();
|
||||
|_, mut cx| async move {
|
||||
let language = language.await.ok();
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
self.editor = editor;
|
||||
self.editor_subscription = editor_subscription;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@ -523,7 +566,7 @@ impl LspLogView {
|
||||
) {
|
||||
self.log_store.update(cx, |log_store, cx| {
|
||||
if enabled {
|
||||
log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
||||
log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
|
||||
} else {
|
||||
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
||||
}
|
||||
@ -535,6 +578,16 @@ impl LspLogView {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_contents(lines: &VecDeque<String>) -> String {
|
||||
let (a, b) = lines.as_slices();
|
||||
let log_contents = a.join("\n");
|
||||
if b.is_empty() {
|
||||
log_contents
|
||||
} else {
|
||||
log_contents + "\n" + &b.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl View for LspLogView {
|
||||
fn ui_name() -> &'static str {
|
||||
"LspLogView"
|
||||
@ -685,6 +738,7 @@ impl View for LspLogToolbarItemView {
|
||||
});
|
||||
let server_selected = current_server.is_some();
|
||||
|
||||
enum LspLogScroll {}
|
||||
enum Menu {}
|
||||
let lsp_menu = Stack::new()
|
||||
.with_child(Self::render_language_server_menu_header(
|
||||
@ -697,7 +751,7 @@ impl View for LspLogToolbarItemView {
|
||||
Overlay::new(
|
||||
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
|
||||
Flex::column()
|
||||
.scrollable::<Self>(0, None, cx)
|
||||
.scrollable::<LspLogScroll>(0, None, cx)
|
||||
.with_children(menu_rows.into_iter().map(|row| {
|
||||
Self::render_language_server_menu_item(
|
||||
row.server_id,
|
||||
@ -876,6 +930,7 @@ impl LspLogToolbarItemView {
|
||||
) -> impl Element<Self> {
|
||||
enum ActivateLog {}
|
||||
enum ActivateRpcTrace {}
|
||||
enum LanguageServerCheckbox {}
|
||||
|
||||
Flex::column()
|
||||
.with_child({
|
||||
@ -921,7 +976,7 @@ impl LspLogToolbarItemView {
|
||||
.with_height(theme.toolbar_dropdown_menu.row_height),
|
||||
)
|
||||
.with_child(
|
||||
ui::checkbox_with_label::<Self, _, Self, _>(
|
||||
ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
|
||||
Empty::new(),
|
||||
&theme.welcome.checkbox,
|
||||
rpc_trace_enabled,
|
||||
@ -947,8 +1002,16 @@ impl LspLogToolbarItemView {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
NewServerLogEntry {
|
||||
id: LanguageServerId,
|
||||
entry: String,
|
||||
is_rpc: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for LogStore {
|
||||
type Event = ();
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl Entity for LspLogView {
|
||||
|
@ -91,9 +91,8 @@ impl TestServer {
|
||||
let identity = claims.sub.unwrap().to_string();
|
||||
let room_name = claims.video.room.unwrap();
|
||||
let mut server_rooms = self.rooms.lock();
|
||||
let room = server_rooms
|
||||
.get_mut(&*room_name)
|
||||
.ok_or_else(|| anyhow!("room {:?} does not exist", room_name))?;
|
||||
let room = (*server_rooms).entry(room_name.to_string()).or_default();
|
||||
|
||||
if room.client_rooms.contains_key(&identity) {
|
||||
Err(anyhow!(
|
||||
"{:?} attempted to join room {:?} twice",
|
||||
|
@ -466,7 +466,10 @@ impl LanguageServer {
|
||||
completion_item: Some(CompletionItemCapability {
|
||||
snippet_support: Some(true),
|
||||
resolve_support: Some(CompletionItemCapabilityResolveSupport {
|
||||
properties: vec!["additionalTextEdits".to_string()],
|
||||
properties: vec![
|
||||
"documentation".to_string(),
|
||||
"additionalTextEdits".to_string(),
|
||||
],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@ -748,6 +751,15 @@ impl LanguageServer {
|
||||
)
|
||||
}
|
||||
|
||||
// some child of string literal (be it "" or ``) which is the child of an attribute
|
||||
|
||||
// <Foo className="bar" />
|
||||
// <Foo className={`bar`} />
|
||||
// <Foo className={something + "bar"} />
|
||||
// <Foo className={something + "bar"} />
|
||||
// const classes = "awesome ";
|
||||
// <Foo className={classes} />
|
||||
|
||||
fn request_internal<T: request::Request>(
|
||||
next_id: &AtomicUsize,
|
||||
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
|
||||
|
@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeNodeRuntime;
|
||||
pub struct FakeNodeRuntime(Option<PrettierSupport>);
|
||||
|
||||
struct PrettierSupport {
|
||||
plugins: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl FakeNodeRuntime {
|
||||
pub fn new() -> Arc<dyn NodeRuntime> {
|
||||
Arc::new(FakeNodeRuntime)
|
||||
Arc::new(FakeNodeRuntime(None))
|
||||
}
|
||||
|
||||
pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
|
||||
Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NodeRuntime for FakeNodeRuntime {
|
||||
async fn binary_path(&self) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
|
||||
if let Some(prettier_support) = &self.0 {
|
||||
prettier_support.binary_path().await
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_npm_subcommand(
|
||||
&self,
|
||||
directory: Option<&Path>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> anyhow::Result<Output> {
|
||||
if let Some(prettier_support) = &self.0 {
|
||||
prettier_support
|
||||
.run_npm_subcommand(directory, subcommand, args)
|
||||
.await
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
|
||||
if let Some(prettier_support) = &self.0 {
|
||||
prettier_support.npm_package_latest_version(name).await
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
async fn npm_install_packages(
|
||||
&self,
|
||||
directory: &Path,
|
||||
packages: &[(&str, &str)],
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(prettier_support) = &self.0 {
|
||||
prettier_support
|
||||
.npm_install_packages(directory, packages)
|
||||
.await
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrettierSupport {
|
||||
const PACKAGE_VERSION: &str = "0.0.1";
|
||||
|
||||
fn new(plugins: &[&'static str]) -> Self {
|
||||
Self {
|
||||
plugins: plugins.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NodeRuntime for PrettierSupport {
|
||||
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
|
||||
Ok(PathBuf::from("prettier_fake_node"))
|
||||
}
|
||||
|
||||
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
|
||||
unreachable!()
|
||||
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
|
||||
if name == "prettier" || self.plugins.contains(&name) {
|
||||
Ok(Self::PACKAGE_VERSION.to_string())
|
||||
} else {
|
||||
panic!("Unexpected package name: {name}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
|
||||
unreachable!()
|
||||
async fn npm_install_packages(
|
||||
&self,
|
||||
_: &Path,
|
||||
packages: &[(&str, &str)],
|
||||
) -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
packages.len(),
|
||||
self.plugins.len() + 1,
|
||||
"Unexpected packages length to install: {:?}, expected `prettier` + {:?}",
|
||||
packages,
|
||||
self.plugins
|
||||
);
|
||||
for (name, version) in packages {
|
||||
assert!(
|
||||
name == &"prettier" || self.plugins.contains(name),
|
||||
"Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
|
||||
name,
|
||||
packages,
|
||||
Self::PACKAGE_VERSION,
|
||||
self.plugins
|
||||
);
|
||||
assert_eq!(
|
||||
version,
|
||||
&Self::PACKAGE_VERSION,
|
||||
"Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
|
||||
version,
|
||||
packages,
|
||||
Self::PACKAGE_VERSION,
|
||||
self.plugins
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
34
crates/prettier/Cargo.toml
Normal file
34
crates/prettier/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "prettier"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/prettier.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections"}
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
fs = { path = "../fs" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
513
crates/prettier/src/prettier.rs
Normal file
513
crates/prettier/src/prettier.rs
Normal file
@ -0,0 +1,513 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncAppContext, ModelHandle};
|
||||
use language::language_settings::language_settings;
|
||||
use language::{Buffer, BundledFormatter, Diff};
|
||||
use lsp::{LanguageServer, LanguageServerId};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::paths::DEFAULT_PRETTIER_DIR;
|
||||
|
||||
pub enum Prettier {
|
||||
Real(RealPrettier),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Test(TestPrettier),
|
||||
}
|
||||
|
||||
pub struct RealPrettier {
|
||||
worktree_id: Option<usize>,
|
||||
default: bool,
|
||||
prettier_dir: PathBuf,
|
||||
server: Arc<LanguageServer>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct TestPrettier {
|
||||
worktree_id: Option<usize>,
|
||||
prettier_dir: PathBuf,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocateStart {
|
||||
pub worktree_root_path: Arc<Path>,
|
||||
pub starting_path: Arc<Path>,
|
||||
}
|
||||
|
||||
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||
const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
|
||||
|
||||
impl Prettier {
|
||||
pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
|
||||
".prettierrc",
|
||||
".prettierrc.json",
|
||||
".prettierrc.json5",
|
||||
".prettierrc.yaml",
|
||||
".prettierrc.yml",
|
||||
".prettierrc.toml",
|
||||
".prettierrc.js",
|
||||
".prettierrc.cjs",
|
||||
"package.json",
|
||||
"prettier.config.js",
|
||||
"prettier.config.cjs",
|
||||
".editorconfig",
|
||||
];
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
|
||||
|
||||
pub async fn locate(
|
||||
starting_path: Option<LocateStart>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
let paths_to_check = match starting_path.as_ref() {
|
||||
Some(starting_path) => {
|
||||
let worktree_root = starting_path
|
||||
.worktree_root_path
|
||||
.components()
|
||||
.into_iter()
|
||||
.take_while(|path_component| {
|
||||
path_component.as_os_str().to_string_lossy() != "node_modules"
|
||||
})
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if worktree_root != starting_path.worktree_root_path.as_ref() {
|
||||
vec![worktree_root]
|
||||
} else {
|
||||
let (worktree_root_metadata, start_path_metadata) = if starting_path
|
||||
.starting_path
|
||||
.as_ref()
|
||||
== Path::new("")
|
||||
{
|
||||
let worktree_root_data =
|
||||
fs.metadata(&worktree_root).await.with_context(|| {
|
||||
format!(
|
||||
"FS metadata fetch for worktree root path {worktree_root:?}",
|
||||
)
|
||||
})?;
|
||||
(worktree_root_data.unwrap_or_else(|| {
|
||||
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
|
||||
}), None)
|
||||
} else {
|
||||
let full_starting_path = worktree_root.join(&starting_path.starting_path);
|
||||
let (worktree_root_data, start_path_data) = futures::try_join!(
|
||||
fs.metadata(&worktree_root),
|
||||
fs.metadata(&full_starting_path),
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("FS metadata fetch for starting path {full_starting_path:?}",)
|
||||
})?;
|
||||
(
|
||||
worktree_root_data.unwrap_or_else(|| {
|
||||
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
|
||||
}),
|
||||
start_path_data,
|
||||
)
|
||||
};
|
||||
|
||||
match start_path_metadata {
|
||||
Some(start_path_metadata) => {
|
||||
anyhow::ensure!(worktree_root_metadata.is_dir,
|
||||
"For non-empty start path, worktree root {starting_path:?} should be a directory");
|
||||
anyhow::ensure!(
|
||||
!start_path_metadata.is_dir,
|
||||
"For non-empty start path, it should not be a directory {starting_path:?}"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
!start_path_metadata.is_symlink,
|
||||
"For non-empty start path, it should not be a symlink {starting_path:?}"
|
||||
);
|
||||
|
||||
let file_to_format = starting_path.starting_path.as_ref();
|
||||
let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
|
||||
let mut current_path = worktree_root;
|
||||
for path_component in file_to_format.components().into_iter() {
|
||||
current_path = current_path.join(path_component);
|
||||
paths_to_check.push_front(current_path.clone());
|
||||
if path_component.as_os_str().to_string_lossy() == "node_modules" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
|
||||
Vec::from(paths_to_check)
|
||||
}
|
||||
None => {
|
||||
anyhow::ensure!(
|
||||
!worktree_root_metadata.is_dir,
|
||||
"For empty start path, worktree root should not be a directory {starting_path:?}"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
!worktree_root_metadata.is_symlink,
|
||||
"For empty start path, worktree root should not be a symlink {starting_path:?}"
|
||||
);
|
||||
worktree_root
|
||||
.parent()
|
||||
.map(|path| vec![path.to_path_buf()])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
match find_closest_prettier_dir(paths_to_check, fs.as_ref())
|
||||
.await
|
||||
.with_context(|| format!("finding prettier starting with {starting_path:?}"))?
|
||||
{
|
||||
Some(prettier_dir) => Ok(prettier_dir),
|
||||
None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn start(
|
||||
worktree_id: Option<usize>,
|
||||
_: LanguageServerId,
|
||||
prettier_dir: PathBuf,
|
||||
_: Arc<dyn NodeRuntime>,
|
||||
_: AsyncAppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(TestPrettier {
|
||||
worktree_id,
|
||||
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
|
||||
prettier_dir,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub async fn start(
|
||||
worktree_id: Option<usize>,
|
||||
server_id: LanguageServerId,
|
||||
prettier_dir: PathBuf,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
cx: AsyncAppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
use lsp::LanguageServerBinary;
|
||||
|
||||
let backgroud = cx.background();
|
||||
anyhow::ensure!(
|
||||
prettier_dir.is_dir(),
|
||||
"Prettier dir {prettier_dir:?} is not a directory"
|
||||
);
|
||||
let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
|
||||
anyhow::ensure!(
|
||||
prettier_server.is_file(),
|
||||
"no prettier server package found at {prettier_server:?}"
|
||||
);
|
||||
|
||||
let node_path = backgroud
|
||||
.spawn(async move { node.binary_path().await })
|
||||
.await?;
|
||||
let server = LanguageServer::new(
|
||||
server_id,
|
||||
LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
|
||||
},
|
||||
Path::new("/"),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.context("prettier server creation")?;
|
||||
let server = backgroud
|
||||
.spawn(server.initialize(None))
|
||||
.await
|
||||
.context("prettier server initialization")?;
|
||||
Ok(Self::Real(RealPrettier {
|
||||
worktree_id,
|
||||
server,
|
||||
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
|
||||
prettier_dir,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn format(
|
||||
&self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
buffer_path: Option<PathBuf>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> anyhow::Result<Diff> {
|
||||
match self {
|
||||
Self::Real(local) => {
|
||||
let params = buffer.read_with(cx, |buffer, cx| {
|
||||
let buffer_language = buffer.language();
|
||||
let parsers_with_plugins = buffer_language
|
||||
.into_iter()
|
||||
.flat_map(|language| {
|
||||
language
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
.filter_map(|formatter| match formatter {
|
||||
BundledFormatter::Prettier {
|
||||
parser_name,
|
||||
plugin_names,
|
||||
} => Some((parser_name, plugin_names)),
|
||||
})
|
||||
})
|
||||
.fold(
|
||||
HashMap::default(),
|
||||
|mut parsers_with_plugins, (parser_name, plugins)| {
|
||||
match parser_name {
|
||||
Some(parser_name) => parsers_with_plugins
|
||||
.entry(parser_name)
|
||||
.or_insert_with(HashSet::default)
|
||||
.extend(plugins),
|
||||
None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
|
||||
existing_plugins.extend(plugins.iter());
|
||||
}),
|
||||
}
|
||||
parsers_with_plugins
|
||||
},
|
||||
);
|
||||
|
||||
let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
|
||||
if parsers_with_plugins.len() > 1 {
|
||||
log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
|
||||
}
|
||||
|
||||
let prettier_node_modules = self.prettier_dir().join("node_modules");
|
||||
anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
|
||||
let plugin_name_into_path = |plugin_name: &str| {
|
||||
let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
|
||||
for possible_plugin_path in [
|
||||
prettier_plugin_dir.join("dist").join("index.mjs"),
|
||||
prettier_plugin_dir.join("dist").join("index.js"),
|
||||
prettier_plugin_dir.join("dist").join("plugin.js"),
|
||||
prettier_plugin_dir.join("index.mjs"),
|
||||
prettier_plugin_dir.join("index.js"),
|
||||
prettier_plugin_dir.join("plugin.js"),
|
||||
prettier_plugin_dir,
|
||||
] {
|
||||
if possible_plugin_path.is_file() {
|
||||
return Some(possible_plugin_path);
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
let (parser, located_plugins) = match selected_parser_with_plugins {
|
||||
Some((parser, plugins)) => {
|
||||
// Tailwind plugin requires being added last
|
||||
// https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
|
||||
let mut add_tailwind_back = false;
|
||||
|
||||
let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
|
||||
if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
|
||||
add_tailwind_back = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
|
||||
if add_tailwind_back {
|
||||
plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
|
||||
}
|
||||
(Some(parser.to_string()), plugins)
|
||||
},
|
||||
None => (None, Vec::new()),
|
||||
};
|
||||
|
||||
let prettier_options = if self.is_default() {
|
||||
let language_settings = language_settings(buffer_language, buffer.file(), cx);
|
||||
let mut options = language_settings.prettier.clone();
|
||||
if !options.contains_key("tabWidth") {
|
||||
options.insert(
|
||||
"tabWidth".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(
|
||||
language_settings.tab_size.get(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
if !options.contains_key("printWidth") {
|
||||
options.insert(
|
||||
"printWidth".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(
|
||||
language_settings.preferred_line_length,
|
||||
)),
|
||||
);
|
||||
}
|
||||
Some(options)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
|
||||
match located_plugin_path {
|
||||
Some(path) => Some(path),
|
||||
None => {
|
||||
log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
|
||||
None},
|
||||
}
|
||||
}).collect();
|
||||
log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
|
||||
|
||||
anyhow::Ok(FormatParams {
|
||||
text: buffer.text(),
|
||||
options: FormatOptions {
|
||||
parser,
|
||||
plugins,
|
||||
path: buffer_path,
|
||||
prettier_options,
|
||||
},
|
||||
})
|
||||
}).context("prettier params calculation")?;
|
||||
let response = local
|
||||
.server
|
||||
.request::<Format>(params)
|
||||
.await
|
||||
.context("prettier format request")?;
|
||||
let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
|
||||
Ok(diff_task.await)
|
||||
}
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(_) => Ok(buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
|
||||
buffer.diff(formatted_text, cx)
|
||||
})
|
||||
.await),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_cache(&self) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Self::Real(local) => local
|
||||
.server
|
||||
.request::<ClearCache>(())
|
||||
.await
|
||||
.context("prettier clear cache"),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(&self) -> Option<&Arc<LanguageServer>> {
|
||||
match self {
|
||||
Self::Real(local) => Some(&local.server),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
match self {
|
||||
Self::Real(local) => local.default,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(test_prettier) => test_prettier.default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prettier_dir(&self) -> &Path {
|
||||
match self {
|
||||
Self::Real(local) => &local.prettier_dir,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(test_prettier) => &test_prettier.prettier_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn worktree_id(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Real(local) => local.worktree_id,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Self::Test(test_prettier) => test_prettier.worktree_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_closest_prettier_dir(
|
||||
paths_to_check: Vec<PathBuf>,
|
||||
fs: &dyn Fs,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
for path in paths_to_check {
|
||||
let possible_package_json = path.join("package.json");
|
||||
if let Some(package_json_metadata) = fs
|
||||
.metadata(&possible_package_json)
|
||||
.await
|
||||
.with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
|
||||
{
|
||||
if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
|
||||
let package_json_contents = fs
|
||||
.load(&possible_package_json)
|
||||
.await
|
||||
.with_context(|| format!("reading {possible_package_json:?} file contents"))?;
|
||||
if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
|
||||
&package_json_contents,
|
||||
) {
|
||||
if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
|
||||
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
|
||||
{
|
||||
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
|
||||
if let Some(node_modules_location_metadata) = fs
|
||||
.metadata(&possible_node_modules_location)
|
||||
.await
|
||||
.with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
|
||||
{
|
||||
if node_modules_location_metadata.is_dir {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
enum Format {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FormatParams {
|
||||
text: String,
|
||||
options: FormatOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FormatOptions {
|
||||
plugins: Vec<PathBuf>,
|
||||
parser: Option<String>,
|
||||
#[serde(rename = "filepath")]
|
||||
path: Option<PathBuf>,
|
||||
prettier_options: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct FormatResult {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for Format {
|
||||
type Params = FormatParams;
|
||||
type Result = FormatResult;
|
||||
const METHOD: &'static str = "prettier/format";
|
||||
}
|
||||
|
||||
enum ClearCache {}
|
||||
|
||||
impl lsp::request::Request for ClearCache {
|
||||
type Params = ();
|
||||
type Result = ();
|
||||
const METHOD: &'static str = "prettier/clear_cache";
|
||||
}
|
217
crates/prettier/src/prettier_server.js
Normal file
217
crates/prettier/src/prettier_server.js
Normal file
@ -0,0 +1,217 @@
|
||||
const { Buffer } = require('buffer');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { once } = require('events');
|
||||
|
||||
const prettierContainerPath = process.argv[2];
|
||||
if (prettierContainerPath == null || prettierContainerPath.length == 0) {
|
||||
process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.stat(prettierContainerPath, (err, stats) => {
|
||||
if (err) {
|
||||
process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier');
|
||||
|
||||
class Prettier {
|
||||
constructor(path, prettier, config) {
|
||||
this.path = path;
|
||||
this.prettier = prettier;
|
||||
this.config = config;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let prettier;
|
||||
let config;
|
||||
try {
|
||||
prettier = await loadPrettier(prettierPath);
|
||||
config = await prettier.resolveConfig(prettierPath) || {};
|
||||
} catch (e) {
|
||||
process.stderr.write(`Failed to load prettier: ${e}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`);
|
||||
process.stdin.resume();
|
||||
handleBuffer(new Prettier(prettierPath, prettier, config));
|
||||
})()
|
||||
|
||||
async function handleBuffer(prettier) {
|
||||
for await (const messageText of readStdin()) {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(messageText);
|
||||
} catch (e) {
|
||||
sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
|
||||
continue;
|
||||
}
|
||||
// allow concurrent request handling by not `await`ing the message handling promise (async function)
|
||||
handleMessage(message, prettier).catch(e => {
|
||||
sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const headerSeparator = "\r\n";
|
||||
const contentLengthHeaderName = 'Content-Length';
|
||||
|
||||
async function* readStdin() {
|
||||
let buffer = Buffer.alloc(0);
|
||||
let streamEnded = false;
|
||||
process.stdin.on('end', () => {
|
||||
streamEnded = true;
|
||||
});
|
||||
process.stdin.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
});
|
||||
|
||||
async function handleStreamEnded(errorMessage) {
|
||||
sendResponse(makeError(errorMessage));
|
||||
buffer = Buffer.alloc(0);
|
||||
messageLength = null;
|
||||
await once(process.stdin, 'readable');
|
||||
streamEnded = false;
|
||||
}
|
||||
|
||||
try {
|
||||
let headersLength = null;
|
||||
let messageLength = null;
|
||||
main_loop: while (true) {
|
||||
if (messageLength === null) {
|
||||
while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
|
||||
if (streamEnded) {
|
||||
await handleStreamEnded('Unexpected end of stream: headers not found');
|
||||
continue main_loop;
|
||||
} else if (buffer.length > contentLengthHeaderName.length * 10) {
|
||||
await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`);
|
||||
continue main_loop;
|
||||
}
|
||||
await once(process.stdin, 'readable');
|
||||
}
|
||||
const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii');
|
||||
const contentLengthHeader = headers.split(headerSeparator)
|
||||
.map(header => header.split(':'))
|
||||
.filter(header => header[2] === undefined)
|
||||
.filter(header => (header[1] || '').length > 0)
|
||||
.find(header => (header[0] || '').trim() === contentLengthHeaderName);
|
||||
const contentLength = (contentLengthHeader || [])[1];
|
||||
if (contentLength === undefined) {
|
||||
await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`);
|
||||
continue main_loop;
|
||||
}
|
||||
headersLength = headers.length + headerSeparator.length * 2;
|
||||
messageLength = parseInt(contentLength, 10);
|
||||
}
|
||||
|
||||
while (buffer.length < (headersLength + messageLength)) {
|
||||
if (streamEnded) {
|
||||
await handleStreamEnded(
|
||||
`Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`);
|
||||
continue main_loop;
|
||||
}
|
||||
await once(process.stdin, 'readable');
|
||||
}
|
||||
|
||||
const messageEnd = headersLength + messageLength;
|
||||
const message = buffer.subarray(headersLength, messageEnd);
|
||||
buffer = buffer.subarray(messageEnd);
|
||||
headersLength = null;
|
||||
messageLength = null;
|
||||
yield message.toString('utf8');
|
||||
}
|
||||
} catch (e) {
|
||||
sendResponse(makeError(`Error reading stdin: ${e}`));
|
||||
} finally {
|
||||
process.stdin.off('data', () => { });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessage(message, prettier) {
|
||||
const { method, id, params } = message;
|
||||
if (method === undefined) {
|
||||
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||
}
|
||||
if (id === undefined) {
|
||||
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
||||
if (method === 'prettier/format') {
|
||||
if (params === undefined || params.text === undefined) {
|
||||
throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`);
|
||||
}
|
||||
if (params.options === undefined) {
|
||||
throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
||||
let resolvedConfig = {};
|
||||
if (params.options.filepath !== undefined) {
|
||||
resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {};
|
||||
}
|
||||
|
||||
const options = {
|
||||
...(params.options.prettierOptions || prettier.config),
|
||||
...resolvedConfig,
|
||||
parser: params.options.parser,
|
||||
plugins: params.options.plugins,
|
||||
path: params.options.filepath
|
||||
};
|
||||
process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`);
|
||||
const formattedText = await prettier.prettier.format(params.text, options);
|
||||
sendResponse({ id, result: { text: formattedText } });
|
||||
} else if (method === 'prettier/clear_cache') {
|
||||
prettier.prettier.clearConfigCache();
|
||||
prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {};
|
||||
sendResponse({ id, result: null });
|
||||
} else if (method === 'initialize') {
|
||||
sendResponse({
|
||||
id,
|
||||
result: {
|
||||
"capabilities": {}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unknown method: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
function makeError(message) {
|
||||
return {
|
||||
error: {
|
||||
"code": -32600, // invalid request code
|
||||
message,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sendResponse(response) {
|
||||
const responsePayloadString = JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
...response
|
||||
});
|
||||
const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`;
|
||||
process.stdout.write(headers + responsePayloadString);
|
||||
}
|
||||
|
||||
function loadPrettier(prettierPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.access(prettierPath, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
|
||||
} else {
|
||||
try {
|
||||
resolve(require(prettierPath));
|
||||
} catch (err) {
|
||||
reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -15,6 +15,7 @@ test-support = [
|
||||
"language/test-support",
|
||||
"settings/test-support",
|
||||
"text/test-support",
|
||||
"prettier/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@ -31,6 +32,8 @@ git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
prettier = { path = "../prettier" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
prettier = { path = "../prettier", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
git2.workspace = true
|
||||
|
@ -10,7 +10,7 @@ use futures::future;
|
||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||
use language::{
|
||||
language_settings::{language_settings, InlayHintKind},
|
||||
point_from_lsp, point_to_lsp,
|
||||
point_from_lsp, point_to_lsp, prepare_completion_documentation,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
|
||||
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||
@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions {
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
completions: Option<lsp::CompletionResponse>,
|
||||
_: ModelHandle<Project>,
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
cx: AsyncAppContext,
|
||||
@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let completions = buffer.read_with(&cx, |buffer, _| {
|
||||
let completions = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let language = buffer.language().cloned();
|
||||
let snapshot = buffer.snapshot();
|
||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||
@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions {
|
||||
completions
|
||||
.into_iter()
|
||||
.filter_map(move |mut lsp_completion| {
|
||||
if let Some(response_list) = &response_list {
|
||||
if let Some(item_defaults) = &response_list.item_defaults {
|
||||
if let Some(data) = &item_defaults.data {
|
||||
lsp_completion.data = Some(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
|
||||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions {
|
||||
}
|
||||
};
|
||||
|
||||
let language = language.clone();
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let language_registry = language_registry.clone();
|
||||
let language = language.clone();
|
||||
|
||||
Some(async move {
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
if let Some(language) = language.as_ref() {
|
||||
language.process_completion(&mut lsp_completion).await;
|
||||
label = language.label_for_completion(&lsp_completion).await;
|
||||
}
|
||||
|
||||
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
|
||||
Some(
|
||||
prepare_completion_documentation(
|
||||
lsp_docs,
|
||||
&language_registry,
|
||||
language.clone(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Completion {
|
||||
old_range,
|
||||
new_text,
|
||||
@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation,
|
||||
server_id,
|
||||
lsp_completion,
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ use futures::{
|
||||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
future::{try_join_all, Shared},
|
||||
future::{self, try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
@ -31,17 +31,19 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||
language_settings::{
|
||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
||||
},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
serialize_anchor, serialize_version, split_operations,
|
||||
},
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
|
||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
|
||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
|
||||
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
|
||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
|
||||
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
@ -49,7 +51,9 @@ use lsp::{
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use postage::watch;
|
||||
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
@ -75,10 +79,13 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminals::Terminals;
|
||||
use text::Anchor;
|
||||
use text::{Anchor, LineEnding, Rope};
|
||||
use util::{
|
||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||
debug_panic, defer,
|
||||
http::HttpClient,
|
||||
merge_json_value_into,
|
||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
||||
post_inc, ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
@ -152,6 +159,11 @@ pub struct Project {
|
||||
copilot_lsp_subscription: Option<gpui::Subscription>,
|
||||
copilot_log_subscription: Option<lsp::Subscription>,
|
||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||
node: Option<Arc<dyn NodeRuntime>>,
|
||||
prettier_instances: HashMap<
|
||||
(Option<WorktreeId>, PathBuf),
|
||||
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
struct DelayedDebounced {
|
||||
@ -580,6 +592,7 @@ impl Project {
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_on_type_formatting);
|
||||
client.add_model_request_handler(Self::handle_inlay_hints);
|
||||
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
|
||||
client.add_model_request_handler(Self::handle_resolve_inlay_hint);
|
||||
client.add_model_request_handler(Self::handle_refresh_inlay_hints);
|
||||
client.add_model_request_handler(Self::handle_reload_buffers);
|
||||
@ -605,6 +618,7 @@ impl Project {
|
||||
|
||||
pub fn local(
|
||||
client: Arc<Client>,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@ -660,6 +674,8 @@ impl Project {
|
||||
copilot_lsp_subscription,
|
||||
copilot_log_subscription: None,
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
node: Some(node),
|
||||
prettier_instances: HashMap::default(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -757,6 +773,8 @@ impl Project {
|
||||
copilot_lsp_subscription,
|
||||
copilot_log_subscription: None,
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
node: None,
|
||||
prettier_instances: HashMap::default(),
|
||||
};
|
||||
for worktree in worktrees {
|
||||
let _ = this.add_worktree(&worktree, cx);
|
||||
@ -795,8 +813,16 @@ impl Project {
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
let project =
|
||||
cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx));
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
client,
|
||||
node_runtime::FakeNodeRuntime::new(),
|
||||
user_store,
|
||||
Arc::new(languages),
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
@ -810,19 +836,37 @@ impl Project {
|
||||
project
|
||||
}
|
||||
|
||||
/// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
|
||||
/// Instead, if appends the suffix to every input, this suffix is returned by this method.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
|
||||
self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
|
||||
plugins,
|
||||
));
|
||||
Prettier::FORMAT_SUFFIX
|
||||
}
|
||||
|
||||
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let mut language_servers_to_start = Vec::new();
|
||||
let mut language_formatters_to_check = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
|
||||
let settings = language_settings(Some(language), Some(file), cx);
|
||||
let buffer_file = File::from_dyn(buffer.file());
|
||||
let buffer_language = buffer.language();
|
||||
let settings = language_settings(buffer_language, buffer.file(), cx);
|
||||
if let Some(language) = buffer_language {
|
||||
if settings.enable_language_server {
|
||||
if let Some(file) = File::from_dyn(Some(file)) {
|
||||
if let Some(file) = buffer_file {
|
||||
language_servers_to_start
|
||||
.push((file.worktree.clone(), language.clone()));
|
||||
.push((file.worktree.clone(), Arc::clone(language)));
|
||||
}
|
||||
}
|
||||
language_formatters_to_check.push((
|
||||
buffer_file.map(|f| f.worktree_id(cx)),
|
||||
Arc::clone(language),
|
||||
settings.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,6 +919,11 @@ impl Project {
|
||||
.detach();
|
||||
}
|
||||
|
||||
for (worktree, language, settings) in language_formatters_to_check {
|
||||
self.install_default_formatters(worktree, &language, &settings, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
// Start all the newly-enabled language servers.
|
||||
for (worktree, language) in language_servers_to_start {
|
||||
let worktree_path = worktree.read(cx).abs_path();
|
||||
@ -2623,7 +2672,26 @@ impl Project {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||
let buffer_file = buffer.read(cx).file().cloned();
|
||||
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||
|
||||
let task_buffer = buffer.clone();
|
||||
let prettier_installation_task =
|
||||
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
||||
cx.spawn(|project, mut cx| async move {
|
||||
prettier_installation_task.await?;
|
||||
let _ = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.prettier_instance_for_buffer(&task_buffer, cx)
|
||||
})
|
||||
.await;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(file) = buffer_file {
|
||||
let worktree = file.worktree.clone();
|
||||
if let Some(tree) = worktree.read(cx).as_local() {
|
||||
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
||||
@ -2684,15 +2752,6 @@ impl Project {
|
||||
let lsp = project_settings.lsp.get(&adapter.name.0);
|
||||
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
|
||||
|
||||
let mut initialization_options = adapter.initialization_options.clone();
|
||||
match (&mut initialization_options, override_options) {
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
merge_json_value_into(override_options, initialization_options);
|
||||
}
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let server_id = pending_server.server_id;
|
||||
let container_dir = pending_server.container_dir.clone();
|
||||
let state = LanguageServerState::Starting({
|
||||
@ -2704,7 +2763,7 @@ impl Project {
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let result = Self::setup_and_insert_language_server(
|
||||
this,
|
||||
initialization_options,
|
||||
override_options,
|
||||
pending_server,
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
@ -2807,7 +2866,7 @@ impl Project {
|
||||
|
||||
async fn setup_and_insert_language_server(
|
||||
this: WeakModelHandle<Self>,
|
||||
initialization_options: Option<serde_json::Value>,
|
||||
override_initialization_options: Option<serde_json::Value>,
|
||||
pending_server: PendingLanguageServer,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
@ -2817,7 +2876,7 @@ impl Project {
|
||||
) -> Result<Option<Arc<LanguageServer>>> {
|
||||
let setup = Self::setup_pending_language_server(
|
||||
this,
|
||||
initialization_options,
|
||||
override_initialization_options,
|
||||
pending_server,
|
||||
adapter.clone(),
|
||||
server_id,
|
||||
@ -2849,7 +2908,7 @@ impl Project {
|
||||
|
||||
async fn setup_pending_language_server(
|
||||
this: WeakModelHandle<Self>,
|
||||
initialization_options: Option<serde_json::Value>,
|
||||
override_options: Option<serde_json::Value>,
|
||||
pending_server: PendingLanguageServer,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
server_id: LanguageServerId,
|
||||
@ -2867,8 +2926,8 @@ impl Project {
|
||||
move |mut params, mut cx| {
|
||||
let this = this;
|
||||
let adapter = adapter.clone();
|
||||
adapter.process_diagnostics(&mut params);
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
adapter.process_diagnostics(&mut params);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_diagnostics(
|
||||
server_id,
|
||||
@ -2995,6 +3054,14 @@ impl Project {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let mut initialization_options = adapter.adapter.initialization_options().await;
|
||||
match (&mut initialization_options, override_options) {
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
merge_json_value_into(override_options, initialization_options);
|
||||
}
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let language_server = language_server.initialize(initialization_options).await?;
|
||||
|
||||
@ -3949,7 +4016,7 @@ impl Project {
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||
if self.is_local() {
|
||||
let mut buffers_with_paths_and_servers = buffers
|
||||
.into_iter()
|
||||
@ -4027,6 +4094,7 @@ impl Project {
|
||||
enum FormatOperation {
|
||||
Lsp(Vec<(Range<Anchor>, String)>),
|
||||
External(Diff),
|
||||
Prettier(Diff),
|
||||
}
|
||||
|
||||
// Apply language-specific formatting using either a language server
|
||||
@ -4062,8 +4130,8 @@ impl Project {
|
||||
| (_, FormatOnSave::External { command, arguments }) => {
|
||||
if let Some(buffer_abs_path) = buffer_abs_path {
|
||||
format_operation = Self::format_via_external_command(
|
||||
&buffer,
|
||||
&buffer_abs_path,
|
||||
buffer,
|
||||
buffer_abs_path,
|
||||
&command,
|
||||
&arguments,
|
||||
&mut cx,
|
||||
@ -4076,6 +4144,69 @@ impl Project {
|
||||
.map(FormatOperation::External);
|
||||
}
|
||||
}
|
||||
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some(prettier_task) = this
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.prettier_instance_for_buffer(buffer, cx)
|
||||
}).await {
|
||||
match prettier_task.await
|
||||
{
|
||||
Ok(prettier) => {
|
||||
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
|
||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||
});
|
||||
format_operation = Some(FormatOperation::Prettier(
|
||||
prettier
|
||||
.format(buffer, buffer_path, &cx)
|
||||
.await
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
Err(e) => anyhow::bail!(
|
||||
"Failed to create prettier instance for buffer during autoformatting: {e:#}"
|
||||
),
|
||||
}
|
||||
} else if let Some((language_server, buffer_abs_path)) =
|
||||
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||
{
|
||||
format_operation = Some(FormatOperation::Lsp(
|
||||
Self::format_via_lsp(
|
||||
&this,
|
||||
&buffer,
|
||||
buffer_abs_path,
|
||||
&language_server,
|
||||
tab_size,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.context("failed to format via language server")?,
|
||||
));
|
||||
}
|
||||
}
|
||||
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some(prettier_task) = this
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.prettier_instance_for_buffer(buffer, cx)
|
||||
}).await {
|
||||
match prettier_task.await
|
||||
{
|
||||
Ok(prettier) => {
|
||||
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
|
||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||
});
|
||||
format_operation = Some(FormatOperation::Prettier(
|
||||
prettier
|
||||
.format(buffer, buffer_path, &cx)
|
||||
.await
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
Err(e) => anyhow::bail!(
|
||||
"Failed to create prettier instance for buffer during formatting: {e:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
buffer.update(&mut cx, |b, cx| {
|
||||
@ -4100,6 +4231,9 @@ impl Project {
|
||||
FormatOperation::External(diff) => {
|
||||
b.apply_diff(diff, cx);
|
||||
}
|
||||
FormatOperation::Prettier(diff) => {
|
||||
b.apply_diff(diff, cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(transaction_id) = whitespace_transaction_id {
|
||||
@ -5873,6 +6007,7 @@ impl Project {
|
||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
||||
this.update_prettier_settings(&worktree, changes, cx);
|
||||
cx.emit(Event::WorktreeUpdatedEntries(
|
||||
worktree.read(cx).id(),
|
||||
changes.clone(),
|
||||
@ -6252,6 +6387,69 @@ impl Project {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_prettier_settings(
|
||||
&self,
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||
cx: &mut ModelContext<'_, Project>,
|
||||
) {
|
||||
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
||||
.iter()
|
||||
.map(Path::new)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let prettier_config_file_changed = changes
|
||||
.iter()
|
||||
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
||||
.filter(|(path, _, _)| {
|
||||
!path
|
||||
.components()
|
||||
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
||||
})
|
||||
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
||||
let current_worktree_id = worktree.read(cx).id();
|
||||
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
||||
log::info!(
|
||||
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
||||
);
|
||||
let prettiers_to_reload = self
|
||||
.prettier_instances
|
||||
.iter()
|
||||
.filter_map(|((worktree_id, prettier_path), prettier_task)| {
|
||||
if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
|
||||
Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
|
||||
async move {
|
||||
prettier_task.await?
|
||||
.clear_cache()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
|
||||
)
|
||||
})
|
||||
.map_err(Arc::new)
|
||||
}
|
||||
}))
|
||||
.await
|
||||
{
|
||||
if let Err(e) = task_result {
|
||||
log::error!("Failed to clear cache for prettier: {e:#}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
@ -7155,6 +7353,40 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_documentation(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ResolveCompletionDocumentationResponse> {
|
||||
let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
|
||||
|
||||
let completion = this
|
||||
.read_with(&mut cx, |this, _| {
|
||||
let id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
let Some(server) = this.language_server_for_id(id) else {
|
||||
return Err(anyhow!("No language server {id}"));
|
||||
};
|
||||
|
||||
Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let mut is_markdown = false;
|
||||
let text = match completion.documentation {
|
||||
Some(lsp::Documentation::String(text)) => text,
|
||||
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
|
||||
is_markdown = kind == lsp::MarkupKind::Markdown;
|
||||
value
|
||||
}
|
||||
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
|
||||
}
|
||||
|
||||
async fn handle_apply_code_action(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ApplyCodeAction>,
|
||||
@ -8109,6 +8341,236 @@ impl Project {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn prettier_instance_for_buffer(
|
||||
&mut self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_file = buffer.file();
|
||||
let Some(buffer_language) = buffer.language() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
if !buffer_language
|
||||
.lsp_adapters()
|
||||
.iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
.any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
|
||||
{
|
||||
return Task::ready(None);
|
||||
}
|
||||
|
||||
let buffer_file = File::from_dyn(buffer_file);
|
||||
let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
|
||||
let worktree_path = buffer_file
|
||||
.as_ref()
|
||||
.and_then(|file| Some(file.worktree.read(cx).abs_path()));
|
||||
let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
|
||||
if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
|
||||
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
|
||||
let prettier_dir = match cx
|
||||
.background()
|
||||
.spawn(Prettier::locate(
|
||||
worktree_path.zip(buffer_path).map(
|
||||
|(worktree_root_path, starting_path)| LocateStart {
|
||||
worktree_root_path,
|
||||
starting_path,
|
||||
},
|
||||
),
|
||||
fs,
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
return Some(
|
||||
Task::ready(Err(Arc::new(e.context(
|
||||
"determining prettier path for worktree {worktree_path:?}",
|
||||
))))
|
||||
.shared(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
|
||||
project
|
||||
.prettier_instances
|
||||
.get(&(worktree_id, prettier_dir.clone()))
|
||||
.cloned()
|
||||
}) {
|
||||
return Some(existing_prettier);
|
||||
}
|
||||
|
||||
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
||||
let task_prettier_dir = prettier_dir.clone();
|
||||
let weak_project = this.downgrade();
|
||||
let new_server_id =
|
||||
this.update(&mut cx, |this, _| this.languages.next_language_server_id());
|
||||
let new_prettier_task = cx
|
||||
.spawn(|mut cx| async move {
|
||||
let prettier = Prettier::start(
|
||||
worktree_id.map(|id| id.to_usize()),
|
||||
new_server_id,
|
||||
task_prettier_dir,
|
||||
node,
|
||||
cx.clone(),
|
||||
)
|
||||
.await
|
||||
.context("prettier start")
|
||||
.map_err(Arc::new)?;
|
||||
log::info!("Started prettier in {:?}", prettier.prettier_dir());
|
||||
|
||||
if let Some((project, prettier_server)) =
|
||||
weak_project.upgrade(&mut cx).zip(prettier.server())
|
||||
{
|
||||
project.update(&mut cx, |project, cx| {
|
||||
let name = if prettier.is_default() {
|
||||
LanguageServerName(Arc::from("prettier (default)"))
|
||||
} else {
|
||||
let prettier_dir = prettier.prettier_dir();
|
||||
let worktree_path = prettier
|
||||
.worktree_id()
|
||||
.map(WorktreeId::from_usize)
|
||||
.and_then(|id| project.worktree_for_id(id, cx))
|
||||
.map(|worktree| worktree.read(cx).abs_path());
|
||||
match worktree_path {
|
||||
Some(worktree_path) => {
|
||||
if worktree_path.as_ref() == prettier_dir {
|
||||
LanguageServerName(Arc::from(format!(
|
||||
"prettier ({})",
|
||||
prettier_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or_default()
|
||||
)))
|
||||
} else {
|
||||
let dir_to_display = match prettier_dir
|
||||
.strip_prefix(&worktree_path)
|
||||
.ok()
|
||||
{
|
||||
Some(relative_path) => relative_path,
|
||||
None => prettier_dir,
|
||||
};
|
||||
LanguageServerName(Arc::from(format!(
|
||||
"prettier ({})",
|
||||
dir_to_display.display(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
None => LanguageServerName(Arc::from(format!(
|
||||
"prettier ({})",
|
||||
prettier_dir.display(),
|
||||
))),
|
||||
}
|
||||
};
|
||||
|
||||
project
|
||||
.supplementary_language_servers
|
||||
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
||||
cx.emit(Event::LanguageServerAdded(new_server_id));
|
||||
});
|
||||
}
|
||||
Ok(Arc::new(prettier)).map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
this.update(&mut cx, |project, _| {
|
||||
project
|
||||
.prettier_instances
|
||||
.insert((worktree_id, prettier_dir), new_prettier_task.clone());
|
||||
});
|
||||
Some(new_prettier_task)
|
||||
})
|
||||
} else if self.remote_id().is_some() {
|
||||
return Task::ready(None);
|
||||
} else {
|
||||
Task::ready(Some(
|
||||
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn install_default_formatters(
|
||||
&self,
|
||||
worktree: Option<WorktreeId>,
|
||||
new_language: &Language,
|
||||
language_settings: &LanguageSettings,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
match &language_settings.formatter {
|
||||
Formatter::Prettier { .. } | Formatter::Auto => {}
|
||||
Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())),
|
||||
};
|
||||
let Some(node) = self.node.as_ref().cloned() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let mut prettier_plugins = None;
|
||||
for formatter in new_language
|
||||
.lsp_adapters()
|
||||
.into_iter()
|
||||
.flat_map(|adapter| adapter.enabled_formatters())
|
||||
{
|
||||
match formatter {
|
||||
BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
|
||||
.get_or_insert_with(|| HashSet::default())
|
||||
.extend(plugin_names),
|
||||
}
|
||||
}
|
||||
let Some(prettier_plugins) = prettier_plugins else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
|
||||
let already_running_prettier = self
|
||||
.prettier_instances
|
||||
.get(&(worktree, default_prettier_dir.to_path_buf()))
|
||||
.cloned();
|
||||
|
||||
let fs = Arc::clone(&self.fs);
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE);
|
||||
// method creates parent directory if it doesn't exist
|
||||
fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await
|
||||
.with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?;
|
||||
|
||||
let packages_to_versions = future::try_join_all(
|
||||
prettier_plugins
|
||||
.iter()
|
||||
.chain(Some(&"prettier"))
|
||||
.map(|package_name| async {
|
||||
let returned_package_name = package_name.to_string();
|
||||
let latest_version = node.npm_package_latest_version(package_name)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("fetching latest npm version for package {returned_package_name}")
|
||||
})?;
|
||||
anyhow::Ok((returned_package_name, latest_version))
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("fetching latest npm versions")?;
|
||||
|
||||
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
||||
let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
|
||||
(package.as_str(), version.as_str())
|
||||
}).collect::<Vec<_>>();
|
||||
node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
|
||||
|
||||
if !prettier_plugins.is_empty() {
|
||||
if let Some(prettier) = already_running_prettier {
|
||||
prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe_for_copilot_events(
|
||||
|
@ -2027,11 +2027,16 @@ impl LocalSnapshot {
|
||||
|
||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||
let mut new_ignores = Vec::new();
|
||||
for ancestor in abs_path.ancestors().skip(1) {
|
||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||
} else {
|
||||
new_ignores.push((ancestor, None));
|
||||
for (index, ancestor) in abs_path.ancestors().enumerate() {
|
||||
if index > 0 {
|
||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||
} else {
|
||||
new_ignores.push((ancestor, None));
|
||||
}
|
||||
}
|
||||
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2048,7 +2053,6 @@ impl LocalSnapshot {
|
||||
if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
|
||||
ignore_stack = IgnoreStack::all();
|
||||
}
|
||||
|
||||
ignore_stack
|
||||
}
|
||||
|
||||
@ -3064,14 +3068,21 @@ impl BackgroundScanner {
|
||||
|
||||
// Populate ignores above the root.
|
||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||
for ancestor in root_abs_path.ancestors().skip(1) {
|
||||
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), false));
|
||||
for (index, ancestor) in root_abs_path.ancestors().enumerate() {
|
||||
if index != 0 {
|
||||
if let Ok(ignore) =
|
||||
build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), false));
|
||||
}
|
||||
}
|
||||
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,88 +89,91 @@ message Envelope {
|
||||
FormatBuffersResponse format_buffers_response = 70;
|
||||
GetCompletions get_completions = 71;
|
||||
GetCompletionsResponse get_completions_response = 72;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
|
||||
GetCodeActions get_code_actions = 75;
|
||||
GetCodeActionsResponse get_code_actions_response = 76;
|
||||
GetHover get_hover = 77;
|
||||
GetHoverResponse get_hover_response = 78;
|
||||
ApplyCodeAction apply_code_action = 79;
|
||||
ApplyCodeActionResponse apply_code_action_response = 80;
|
||||
PrepareRename prepare_rename = 81;
|
||||
PrepareRenameResponse prepare_rename_response = 82;
|
||||
PerformRename perform_rename = 83;
|
||||
PerformRenameResponse perform_rename_response = 84;
|
||||
SearchProject search_project = 85;
|
||||
SearchProjectResponse search_project_response = 86;
|
||||
ResolveCompletionDocumentation resolve_completion_documentation = 73;
|
||||
ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76;
|
||||
GetCodeActions get_code_actions = 77;
|
||||
GetCodeActionsResponse get_code_actions_response = 78;
|
||||
GetHover get_hover = 79;
|
||||
GetHoverResponse get_hover_response = 80;
|
||||
ApplyCodeAction apply_code_action = 81;
|
||||
ApplyCodeActionResponse apply_code_action_response = 82;
|
||||
PrepareRename prepare_rename = 83;
|
||||
PrepareRenameResponse prepare_rename_response = 84;
|
||||
PerformRename perform_rename = 85;
|
||||
PerformRenameResponse perform_rename_response = 86;
|
||||
SearchProject search_project = 87;
|
||||
SearchProjectResponse search_project_response = 88;
|
||||
|
||||
UpdateContacts update_contacts = 87;
|
||||
UpdateInviteInfo update_invite_info = 88;
|
||||
ShowContacts show_contacts = 89;
|
||||
UpdateContacts update_contacts = 89;
|
||||
UpdateInviteInfo update_invite_info = 90;
|
||||
ShowContacts show_contacts = 91;
|
||||
|
||||
GetUsers get_users = 90;
|
||||
FuzzySearchUsers fuzzy_search_users = 91;
|
||||
UsersResponse users_response = 92;
|
||||
RequestContact request_contact = 93;
|
||||
RespondToContactRequest respond_to_contact_request = 94;
|
||||
RemoveContact remove_contact = 95;
|
||||
GetUsers get_users = 92;
|
||||
FuzzySearchUsers fuzzy_search_users = 93;
|
||||
UsersResponse users_response = 94;
|
||||
RequestContact request_contact = 95;
|
||||
RespondToContactRequest respond_to_contact_request = 96;
|
||||
RemoveContact remove_contact = 97;
|
||||
|
||||
Follow follow = 96;
|
||||
FollowResponse follow_response = 97;
|
||||
UpdateFollowers update_followers = 98;
|
||||
Unfollow unfollow = 99;
|
||||
GetPrivateUserInfo get_private_user_info = 100;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 101;
|
||||
UpdateDiffBase update_diff_base = 102;
|
||||
Follow follow = 98;
|
||||
FollowResponse follow_response = 99;
|
||||
UpdateFollowers update_followers = 100;
|
||||
Unfollow unfollow = 101;
|
||||
GetPrivateUserInfo get_private_user_info = 102;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 103;
|
||||
UpdateDiffBase update_diff_base = 104;
|
||||
|
||||
OnTypeFormatting on_type_formatting = 103;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 104;
|
||||
OnTypeFormatting on_type_formatting = 105;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 106;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 105;
|
||||
UpdateWorktreeSettings update_worktree_settings = 107;
|
||||
|
||||
InlayHints inlay_hints = 106;
|
||||
InlayHintsResponse inlay_hints_response = 107;
|
||||
ResolveInlayHint resolve_inlay_hint = 108;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 109;
|
||||
RefreshInlayHints refresh_inlay_hints = 110;
|
||||
InlayHints inlay_hints = 108;
|
||||
InlayHintsResponse inlay_hints_response = 109;
|
||||
ResolveInlayHint resolve_inlay_hint = 110;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 111;
|
||||
RefreshInlayHints refresh_inlay_hints = 112;
|
||||
|
||||
CreateChannel create_channel = 111;
|
||||
CreateChannelResponse create_channel_response = 112;
|
||||
InviteChannelMember invite_channel_member = 113;
|
||||
RemoveChannelMember remove_channel_member = 114;
|
||||
RespondToChannelInvite respond_to_channel_invite = 115;
|
||||
UpdateChannels update_channels = 116;
|
||||
JoinChannel join_channel = 117;
|
||||
DeleteChannel delete_channel = 118;
|
||||
GetChannelMembers get_channel_members = 119;
|
||||
GetChannelMembersResponse get_channel_members_response = 120;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 121;
|
||||
RenameChannel rename_channel = 122;
|
||||
RenameChannelResponse rename_channel_response = 123;
|
||||
CreateChannel create_channel = 113;
|
||||
CreateChannelResponse create_channel_response = 114;
|
||||
InviteChannelMember invite_channel_member = 115;
|
||||
RemoveChannelMember remove_channel_member = 116;
|
||||
RespondToChannelInvite respond_to_channel_invite = 117;
|
||||
UpdateChannels update_channels = 118;
|
||||
JoinChannel join_channel = 119;
|
||||
DeleteChannel delete_channel = 120;
|
||||
GetChannelMembers get_channel_members = 121;
|
||||
GetChannelMembersResponse get_channel_members_response = 122;
|
||||
SetChannelMemberRole set_channel_member_role = 123;
|
||||
RenameChannel rename_channel = 124;
|
||||
RenameChannelResponse rename_channel_response = 125;
|
||||
|
||||
JoinChannelBuffer join_channel_buffer = 124;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 125;
|
||||
UpdateChannelBuffer update_channel_buffer = 126;
|
||||
LeaveChannelBuffer leave_channel_buffer = 127;
|
||||
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 129;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
|
||||
AckBufferOperation ack_buffer_operation = 143;
|
||||
JoinChannelBuffer join_channel_buffer = 126;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 127;
|
||||
UpdateChannelBuffer update_channel_buffer = 128;
|
||||
LeaveChannelBuffer leave_channel_buffer = 129;
|
||||
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 131;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132;
|
||||
AckBufferOperation ack_buffer_operation = 145;
|
||||
|
||||
JoinChannelChat join_channel_chat = 131;
|
||||
JoinChannelChatResponse join_channel_chat_response = 132;
|
||||
LeaveChannelChat leave_channel_chat = 133;
|
||||
SendChannelMessage send_channel_message = 134;
|
||||
SendChannelMessageResponse send_channel_message_response = 135;
|
||||
ChannelMessageSent channel_message_sent = 136;
|
||||
GetChannelMessages get_channel_messages = 137;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 138;
|
||||
RemoveChannelMessage remove_channel_message = 139;
|
||||
AckChannelMessage ack_channel_message = 144;
|
||||
JoinChannelChat join_channel_chat = 133;
|
||||
JoinChannelChatResponse join_channel_chat_response = 134;
|
||||
LeaveChannelChat leave_channel_chat = 135;
|
||||
SendChannelMessage send_channel_message = 136;
|
||||
SendChannelMessageResponse send_channel_message_response = 137;
|
||||
ChannelMessageSent channel_message_sent = 138;
|
||||
GetChannelMessages get_channel_messages = 139;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 140;
|
||||
RemoveChannelMessage remove_channel_message = 141;
|
||||
AckChannelMessage ack_channel_message = 146;
|
||||
|
||||
LinkChannel link_channel = 140;
|
||||
UnlinkChannel unlink_channel = 141;
|
||||
MoveChannel move_channel = 142; // current max: 144
|
||||
LinkChannel link_channel = 142;
|
||||
UnlinkChannel unlink_channel = 143;
|
||||
MoveChannel move_channel = 144;
|
||||
SetChannelVisibility set_channel_visibility = 147; // current max: 147
|
||||
}
|
||||
}
|
||||
|
||||
@ -832,6 +835,17 @@ message ResolveState {
|
||||
}
|
||||
}
|
||||
|
||||
message ResolveCompletionDocumentation {
|
||||
uint64 project_id = 1;
|
||||
uint64 language_server_id = 2;
|
||||
bytes lsp_completion = 3;
|
||||
}
|
||||
|
||||
message ResolveCompletionDocumentationResponse {
|
||||
string text = 1;
|
||||
bool is_markdown = 2;
|
||||
}
|
||||
|
||||
message ResolveInlayHint {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
@ -979,7 +993,7 @@ message ChannelEdge {
|
||||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
bool is_admin = 2;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message ChannelParticipants {
|
||||
@ -1005,8 +1019,8 @@ message GetChannelMembersResponse {
|
||||
|
||||
message ChannelMember {
|
||||
uint64 user_id = 1;
|
||||
bool admin = 2;
|
||||
Kind kind = 3;
|
||||
ChannelRole role = 4;
|
||||
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
@ -1028,7 +1042,7 @@ message CreateChannelResponse {
|
||||
message InviteChannelMember {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 4;
|
||||
}
|
||||
|
||||
message RemoveChannelMember {
|
||||
@ -1036,10 +1050,22 @@ message RemoveChannelMember {
|
||||
uint64 user_id = 2;
|
||||
}
|
||||
|
||||
message SetChannelMemberAdmin {
|
||||
enum ChannelRole {
|
||||
Admin = 0;
|
||||
Member = 1;
|
||||
Guest = 2;
|
||||
Banned = 3;
|
||||
}
|
||||
|
||||
message SetChannelMemberRole {
|
||||
uint64 channel_id = 1;
|
||||
uint64 user_id = 2;
|
||||
bool admin = 3;
|
||||
ChannelRole role = 3;
|
||||
}
|
||||
|
||||
message SetChannelVisibility {
|
||||
uint64 channel_id = 1;
|
||||
ChannelVisibility visibility = 2;
|
||||
}
|
||||
|
||||
message RenameChannel {
|
||||
@ -1533,9 +1559,15 @@ message Nonce {
|
||||
uint64 lower_half = 2;
|
||||
}
|
||||
|
||||
enum ChannelVisibility {
|
||||
Public = 0;
|
||||
Members = 1;
|
||||
}
|
||||
|
||||
message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
@ -205,6 +205,8 @@ messages!(
|
||||
(OnTypeFormattingResponse, Background),
|
||||
(InlayHints, Background),
|
||||
(InlayHintsResponse, Background),
|
||||
(ResolveCompletionDocumentation, Background),
|
||||
(ResolveCompletionDocumentationResponse, Background),
|
||||
(ResolveInlayHint, Background),
|
||||
(ResolveInlayHintResponse, Background),
|
||||
(RefreshInlayHints, Foreground),
|
||||
@ -230,7 +232,8 @@ messages!(
|
||||
(SaveBuffer, Foreground),
|
||||
(RenameChannel, Foreground),
|
||||
(RenameChannelResponse, Foreground),
|
||||
(SetChannelMemberAdmin, Foreground),
|
||||
(SetChannelMemberRole, Foreground),
|
||||
(SetChannelVisibility, Foreground),
|
||||
(SearchProject, Background),
|
||||
(SearchProjectResponse, Background),
|
||||
(ShareProject, Foreground),
|
||||
@ -318,6 +321,10 @@ request_messages!(
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(OnTypeFormatting, OnTypeFormattingResponse),
|
||||
(InlayHints, InlayHintsResponse),
|
||||
(
|
||||
ResolveCompletionDocumentation,
|
||||
ResolveCompletionDocumentationResponse
|
||||
),
|
||||
(ResolveInlayHint, ResolveInlayHintResponse),
|
||||
(RefreshInlayHints, Ack),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
@ -326,7 +333,8 @@ request_messages!(
|
||||
(RemoveContact, Ack),
|
||||
(RespondToContactRequest, Ack),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(SetChannelMemberAdmin, Ack),
|
||||
(SetChannelMemberRole, Ack),
|
||||
(SetChannelVisibility, Ack),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
(GetChannelMembers, GetChannelMembersResponse),
|
||||
@ -381,6 +389,7 @@ entity_messages!(
|
||||
PerformRename,
|
||||
OnTypeFormatting,
|
||||
InlayHints,
|
||||
ResolveCompletionDocumentation,
|
||||
ResolveInlayHint,
|
||||
RefreshInlayHints,
|
||||
PrepareRename,
|
||||
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 64;
|
||||
pub const PROTOCOL_VERSION: u32 = 65;
|
||||
|
@ -537,6 +537,7 @@ impl BufferSearchBar {
|
||||
self.active_searchable_item
|
||||
.as_ref()
|
||||
.map(|searchable_item| searchable_item.query_suggestion(cx))
|
||||
.filter(|suggestion| !suggestion.is_empty())
|
||||
}
|
||||
|
||||
pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
|
||||
|
@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"]}
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
client = { path = "../client" }
|
||||
zed = { path = "../zed"}
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
|
||||
pretty_assertions.workspace = true
|
||||
@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true
|
||||
tree-sitter-lua.workspace = true
|
||||
tree-sitter-ruby.workspace = true
|
||||
tree-sitter-php.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "eval"
|
||||
|
2919
crates/storybook/Cargo.lock
generated
2919
crates/storybook/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "storybook"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "storybook"
|
||||
path = "src/storybook.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap = { version = "4.4", features = ["derive", "string"] }
|
||||
chrono = "0.4"
|
||||
fs = { path = "../fs" }
|
||||
futures.workspace = true
|
||||
gpui2 = { path = "../gpui2" }
|
||||
itertools = "0.11.0"
|
||||
log.workspace = true
|
||||
rust-embed.workspace = true
|
||||
serde.workspace = true
|
||||
settings = { path = "../settings" }
|
||||
simplelog = "0.9"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
util = { path = "../util" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui2 = { path = "../gpui2", features = ["test-support"] }
|
@ -1,72 +0,0 @@
|
||||
Much of element styling is now handled by an external engine.
|
||||
|
||||
|
||||
How do I make an element hover.
|
||||
|
||||
There's a hover style.
|
||||
|
||||
Hoverable needs to wrap another element. That element can be styled.
|
||||
|
||||
```rs
|
||||
struct Hoverable<E: Element> {
|
||||
|
||||
}
|
||||
|
||||
impl<V> Element<V> for Hoverable {
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
```rs
|
||||
#[derive(Styled, Interactive)]
|
||||
pub struct Div {
|
||||
declared_style: StyleRefinement,
|
||||
interactions: Interactions
|
||||
}
|
||||
|
||||
pub trait Styled {
|
||||
fn declared_style(&mut self) -> &mut StyleRefinement;
|
||||
fn compute_style(&mut self) -> Style {
|
||||
Style::default().refine(self.declared_style())
|
||||
}
|
||||
|
||||
// All the tailwind classes, modifying self.declared_style()
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
|
||||
pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
|
||||
}
|
||||
|
||||
pub trait Interactive<V> {
|
||||
fn interactions(&mut self) -> &mut Interactions<V>;
|
||||
|
||||
fn on_click(self, )
|
||||
}
|
||||
|
||||
struct Interactions<V> {
|
||||
click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
```rs
|
||||
|
||||
|
||||
trait Stylable {
|
||||
type Style;
|
||||
|
||||
fn with_style(self, style: Self::Style) -> Self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
pub mod components;
|
||||
pub mod elements;
|
||||
pub mod kitchen_sink;
|
@ -1,22 +0,0 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod breadcrumb;
|
||||
pub mod buffer;
|
||||
pub mod chat_panel;
|
||||
pub mod collab_panel;
|
||||
pub mod context_menu;
|
||||
pub mod facepile;
|
||||
pub mod keybinding;
|
||||
pub mod language_selector;
|
||||
pub mod multi_buffer;
|
||||
pub mod palette;
|
||||
pub mod panel;
|
||||
pub mod project_panel;
|
||||
pub mod recent_projects;
|
||||
pub mod status_bar;
|
||||
pub mod tab;
|
||||
pub mod tab_bar;
|
||||
pub mod terminal;
|
||||
pub mod theme_selector;
|
||||
pub mod title_bar;
|
||||
pub mod toolbar;
|
||||
pub mod traffic_lights;
|
@ -1,16 +0,0 @@
|
||||
use ui::prelude::*;
|
||||
use ui::AssistantPanel;
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element, Default)]
|
||||
pub struct AssistantPanelStory {}
|
||||
|
||||
impl AssistantPanelStory {
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, AssistantPanel<V>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(AssistantPanel::new())
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use ui::prelude::*;
|
||||
use ui::{Breadcrumb, HighlightedText, Symbol};
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element, Default)]
|
||||
pub struct BreadcrumbStory {}
|
||||
|
||||
impl BreadcrumbStory {
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Breadcrumb>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Breadcrumb::new(
|
||||
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
|
||||
vec![
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "impl ".to_string(),
|
||||
color: HighlightColor::Keyword.hsla(&theme),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "BreadcrumbStory".to_string(),
|
||||
color: HighlightColor::Function.hsla(&theme),
|
||||
},
|
||||
]),
|
||||
Symbol(vec![
|
||||
HighlightedText {
|
||||
text: "fn ".to_string(),
|
||||
color: HighlightColor::Keyword.hsla(&theme),
|
||||
},
|
||||
HighlightedText {
|
||||
text: "render".to_string(),
|
||||
color: HighlightColor::Function.hsla(&theme),
|
||||
},
|
||||
]),
|
||||
],
|
||||
))
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
use gpui2::geometry::rems;
|
||||
use ui::prelude::*;
|
||||
use ui::{
|
||||
empty_buffer_example, hello_world_rust_buffer_example,
|
||||
hello_world_rust_buffer_with_status_example, Buffer,
|
||||
};
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element, Default)]
|
||||
pub struct BufferStory {}
|
||||
|
||||
impl BufferStory {
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
let theme = theme(cx);
|
||||
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, Buffer>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
|
||||
.child(Story::label(cx, "Hello World (Rust)"))
|
||||
.child(
|
||||
div()
|
||||
.w(rems(64.))
|
||||
.h_96()
|
||||
.child(hello_world_rust_buffer_example(&theme)),
|
||||
)
|
||||
.child(Story::label(cx, "Hello World (Rust) with Status"))
|
||||
.child(
|
||||
div()
|
||||
.w(rems(64.))
|
||||
.h_96()
|
||||
.child(hello_world_rust_buffer_with_status_example(&theme)),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
use chrono::DateTime;
|
||||
use ui::prelude::*;
|
||||
use ui::{ChatMessage, ChatPanel, Panel};
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element, Default)]
|
||||
pub struct ChatPanelStory {}
|
||||
|
||||
impl ChatPanelStory {
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, ChatPanel<V>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(Panel::new(
|
||||
ScrollState::default(),
|
||||
|_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
|
||||
Box::new(()),
|
||||
))
|
||||
.child(Story::label(cx, "With Mesages"))
|
||||
.child(Panel::new(
|
||||
ScrollState::default(),
|
||||
|_, _| {
|
||||
vec![ChatPanel::new(ScrollState::default())
|
||||
.with_messages(vec![
|
||||
ChatMessage::new(
|
||||
"osiewicz".to_string(),
|
||||
"is this thing on?".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
ChatMessage::new(
|
||||
"maxdeviant".to_string(),
|
||||
"Reading you loud and clear!".to_string(),
|
||||
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
),
|
||||
])
|
||||
.into_any()]
|
||||
},
|
||||
Box::new(()),
|
||||
))
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use ui::prelude::*;
|
||||
use ui::CollabPanel;
|
||||
|
||||
use crate::story::Story;
|
||||
|
||||
#[derive(Element, Default)]
|
||||
pub struct CollabPanelStory {}
|
||||
|
||||
impl CollabPanelStory {
|
||||
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
|
||||
Story::container(cx)
|
||||
.child(Story::title_for::<_, CollabPanel<V>>(cx))
|
||||
.child(Story::label(cx, "Default"))
|
||||
.child(CollabPanel::new(ScrollState::default()))
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user