Merge branch 'haskell-support' into add-haskell-grammar

This commit is contained in:
Pseudomata 2024-01-26 10:55:43 -05:00 committed by GitHub
commit 2b9ba46cb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
146 changed files with 2847 additions and 1749 deletions

View file

@ -1,4 +1,4 @@
[[PR Description]]
Release Notes: Release Notes:

View file

@ -29,7 +29,7 @@ jobs:
- test - test
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
@ -55,7 +55,7 @@ jobs:
- test - test
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
@ -88,7 +88,7 @@ jobs:
node-version: "18" node-version: "18"
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"

View file

@ -26,7 +26,7 @@ jobs:
run: docker system prune run: docker system prune
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: 'recursive' submodules: 'recursive'

View file

@ -28,7 +28,7 @@ jobs:
node-version: "18" node-version: "18"
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"

View file

@ -21,7 +21,7 @@ jobs:
- test - test
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
@ -38,7 +38,7 @@ jobs:
needs: style needs: style
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"
@ -66,7 +66,7 @@ jobs:
node-version: "18" node-version: "18"
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
clean: false clean: false
submodules: "recursive" submodules: "recursive"

View file

@ -7,7 +7,7 @@ jobs:
update_top_ranking_issues: update_top_ranking_issues:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: "3.10.5" python-version: "3.10.5"

View file

@ -11,6 +11,8 @@
Antonio Scandurra <me@as-cii.com> Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev> Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com> Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com> Julia <floc@unpromptedtirade.com>
@ -37,3 +39,6 @@ Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com> Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Piotr Osiewicz <piotr@zed.dev> Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com> Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>

View file

@ -37,8 +37,9 @@ We plan to set aside time each week to pair program with contributors on promisi
- Add test coverage and documentation - Add test coverage and documentation
- Choose tasks that align with our roadmap - Choose tasks that align with our roadmap
- Pair with us and watch us code to learn the codebase - Pair with us and watch us code to learn the codebase
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
## Bird-eye's view of Zed ## Bird's-eye view of Zed
Zed is made up of several smaller crates - let's go over those you're most likely to interact with: Zed is made up of several smaller crates - let's go over those you're most likely to interact with:

385
Cargo.lock generated
View file

@ -101,50 +101,25 @@ dependencies = [
"util", "util",
] ]
[[package]]
name = "alacritty_config"
version = "0.1.2-dev"
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
dependencies = [
"log",
"serde",
"toml 0.7.8",
]
[[package]]
name = "alacritty_config_derive"
version = "0.2.2-dev"
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]] [[package]]
name = "alacritty_terminal" name = "alacritty_terminal"
version = "0.20.0-dev" version = "0.21.0"
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
dependencies = [ dependencies = [
"alacritty_config", "base64 0.21.4",
"alacritty_config_derive",
"base64 0.13.1",
"bitflags 2.4.1", "bitflags 2.4.1",
"home", "home",
"libc", "libc",
"log", "log",
"mio 0.6.23", "miow 0.6.0",
"mio-anonymous-pipes",
"mio-extras",
"miow 0.3.7",
"nix 0.26.4",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"regex-automata 0.1.10", "piper",
"polling 3.3.2",
"regex-automata 0.4.5",
"rustix-openpty",
"serde", "serde",
"serde_yaml",
"signal-hook", "signal-hook",
"signal-hook-mio",
"toml 0.7.8",
"unicode-width", "unicode-width",
"vte", "vte",
"windows-sys 0.48.0", "windows-sys 0.48.0",
@ -444,7 +419,7 @@ dependencies = [
"futures-lite", "futures-lite",
"log", "log",
"parking", "parking",
"polling", "polling 2.8.0",
"rustix 0.37.23", "rustix 0.37.23",
"slab", "slab",
"socket2 0.4.9", "socket2 0.4.9",
@ -1155,7 +1130,7 @@ dependencies = [
"serde_json", "serde_json",
"syn 1.0.109", "syn 1.0.109",
"tempfile", "tempfile",
"toml 0.5.11", "toml",
] ]
[[package]] [[package]]
@ -1423,36 +1398,37 @@ dependencies = [
[[package]] [[package]]
name = "cocoa" name = "cocoa"
version = "0.24.0" version = "0.25.0"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"block", "block",
"cocoa-foundation", "cocoa-foundation",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.23.1",
"foreign-types", "foreign-types 0.5.0",
"libc", "libc",
"objc", "objc",
] ]
[[package]] [[package]]
name = "cocoa-foundation" name = "cocoa-foundation"
version = "0.1.1" version = "0.1.2"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"block", "block",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types",
"libc", "libc",
"objc", "objc",
] ]
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.40.1" version = "0.41.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1514,7 +1490,7 @@ dependencies = [
"time", "time",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"toml 0.5.11", "toml",
"tonic", "tonic",
"tower", "tower",
"tracing", "tracing",
@ -1586,22 +1562,9 @@ dependencies = [
name = "color" name = "color"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"fs",
"indexmap 1.9.3",
"itertools 0.11.0", "itertools 0.11.0",
"palette", "palette",
"parking_lot 0.11.2",
"refineable",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"story", "story",
"toml 0.5.11",
"util",
"uuid 1.4.1",
] ]
[[package]] [[package]]
@ -1750,10 +1713,11 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.4"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [ dependencies = [
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"libc", "libc",
"uuid 0.5.1", "uuid 0.5.1",
] ]
@ -1766,29 +1730,44 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.6"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "core-graphics" name = "core-graphics"
version = "0.22.3" version = "0.22.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.3.2",
"libc",
]
[[package]]
name = "core-graphics"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types 0.5.0",
"libc", "libc",
] ]
[[package]] [[package]]
name = "core-graphics-types" name = "core-graphics-types"
version = "0.1.1" version = "0.1.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"foreign-types",
"libc", "libc",
] ]
@ -1808,8 +1787,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25"
dependencies = [ dependencies = [
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
] ]
@ -1840,7 +1819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
dependencies = [ dependencies = [
"alsa", "alsa",
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"coreaudio-rs", "coreaudio-rs",
"dasp_sample", "dasp_sample",
"jni 0.19.0", "jni 0.19.0",
@ -2014,6 +1993,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "cursor-icon"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.5.3" version = "5.5.3"
@ -2639,7 +2624,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"byteorder", "byteorder",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"core-text", "core-text",
"dirs-next", "dirs-next",
"dwrote", "dwrote",
@ -2672,7 +2657,28 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [ dependencies = [
"foreign-types-shared", "foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
] ]
[[package]] [[package]]
@ -2681,6 +2687,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.0" version = "1.2.0"
@ -3063,7 +3075,7 @@ dependencies = [
"cocoa", "cocoa",
"collections", "collections",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"core-text", "core-text",
"ctor", "ctor",
"derive_more", "derive_more",
@ -3071,7 +3083,7 @@ dependencies = [
"env_logger", "env_logger",
"etagere", "etagere",
"font-kit", "font-kit",
"foreign-types", "foreign-types 0.3.2",
"futures 0.3.28", "futures 0.3.28",
"gpui_macros", "gpui_macros",
"image", "image",
@ -3397,7 +3409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -3580,7 +3592,7 @@ dependencies = [
"log", "log",
"mime", "mime",
"once_cell", "once_cell",
"polling", "polling 2.8.0",
"slab", "slab",
"sluice", "sluice",
"tracing", "tracing",
@ -3920,12 +3932,6 @@ dependencies = [
"safemem", "safemem",
] ]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linkme" name = "linkme"
version = "0.3.17" version = "0.3.17"
@ -3981,8 +3987,8 @@ dependencies = [
"cocoa", "cocoa",
"collections", "collections",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics 0.22.3",
"foreign-types", "foreign-types 0.3.2",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
"hmac 0.12.1", "hmac 0.12.1",
@ -4142,7 +4148,7 @@ dependencies = [
"block", "block",
"bytes 1.5.0", "bytes 1.5.0",
"core-foundation", "core-foundation",
"foreign-types", "foreign-types 0.3.2",
"metal", "metal",
"objc", "objc",
] ]
@ -4197,7 +4203,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"block", "block",
"cocoa-foundation", "cocoa-foundation",
"foreign-types", "foreign-types 0.3.2",
"log", "log",
"objc", "objc",
] ]
@ -4282,19 +4288,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "mio-anonymous-pipes"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b"
dependencies = [
"mio 0.6.23",
"miow 0.3.7",
"parking_lot 0.11.2",
"spsc-buffer",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "mio-extras" name = "mio-extras"
version = "2.0.6" version = "2.0.6"
@ -4307,17 +4300,6 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "mio-uds"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
dependencies = [
"iovec",
"libc",
"mio 0.6.23",
]
[[package]] [[package]]
name = "miow" name = "miow"
version = "0.2.2" version = "0.2.2"
@ -4332,11 +4314,11 @@ dependencies = [
[[package]] [[package]]
name = "miow" name = "miow"
version = "0.3.7" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
dependencies = [ dependencies = [
"winapi 0.3.9", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -4496,17 +4478,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if 1.0.0",
"libc",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.27.1" version = "0.27.1"
@ -4865,7 +4836,7 @@ checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
"once_cell", "once_cell",
"openssl-macros", "openssl-macros",
@ -5277,6 +5248,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
dependencies = [
"atomic-waker",
"fastrand 2.0.0",
"futures-io",
]
[[package]] [[package]]
name = "pkcs1" name = "pkcs1"
version = "0.7.5" version = "0.7.5"
@ -5368,6 +5350,20 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "polling"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41"
dependencies = [
"cfg-if 1.0.0",
"concurrent-queue",
"pin-project-lite 0.2.13",
"rustix 0.38.30",
"tracing",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "pollster" name = "pollster"
version = "0.2.5" version = "0.2.5"
@ -5444,7 +5440,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
dependencies = [ dependencies = [
"toml 0.5.11", "toml",
] ]
[[package]] [[package]]
@ -5551,7 +5547,7 @@ dependencies = [
"terminal", "terminal",
"text", "text",
"thiserror", "thiserror",
"toml 0.5.11", "toml",
"unindent", "unindent",
"util", "util",
] ]
@ -5994,6 +5990,17 @@ dependencies = [
"regex-syntax 0.7.5", "regex-syntax 0.7.5",
] ]
[[package]]
name = "regex-automata"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.2",
]
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.29" version = "0.6.29"
@ -6006,6 +6013,12 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "rend" name = "rend"
version = "0.4.0" version = "0.4.0"
@ -6374,11 +6387,23 @@ checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"errno", "errno",
"itoa",
"libc", "libc",
"linux-raw-sys 0.4.12", "linux-raw-sys 0.4.12",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rustix-openpty"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
dependencies = [
"errno",
"libc",
"rustix 0.38.30",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.19.1" version = "0.19.1"
@ -6694,7 +6719,7 @@ checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
] ]
@ -6705,7 +6730,7 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [ dependencies = [
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"libc", "libc",
] ]
@ -6847,15 +6872,6 @@ dependencies = [
"syn 2.0.37", "syn 2.0.37",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -6868,18 +6884,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
"indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]] [[package]]
name = "settings" name = "settings"
version = "0.1.0" version = "0.1.0"
@ -6901,7 +6905,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_json_lenient", "serde_json_lenient",
"smallvec", "smallvec",
"toml 0.5.11", "toml",
"tree-sitter", "tree-sitter",
"tree-sitter-json 0.19.0", "tree-sitter-json 0.19.0",
"unindent", "unindent",
@ -7007,18 +7011,6 @@ dependencies = [
"signal-hook-registry", "signal-hook-registry",
] ]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio 0.6.23",
"mio-uds",
"signal-hook",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -7209,12 +7201,6 @@ dependencies = [
"der", "der",
] ]
[[package]]
name = "spsc-buffer"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b"
[[package]] [[package]]
name = "sqlez" name = "sqlez"
version = "0.1.0" version = "0.1.0"
@ -7712,7 +7698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"core-foundation-sys 0.8.3", "core-foundation-sys 0.8.6",
"libc", "libc",
"ntapi 0.4.1", "ntapi 0.4.1",
"once_cell", "once_cell",
@ -7878,7 +7864,7 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"story", "story",
"toml 0.5.11", "toml",
"util", "util",
"uuid 1.4.1", "uuid 1.4.1",
] ]
@ -8183,26 +8169,11 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.3" version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
@ -8211,8 +8182,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [ dependencies = [
"indexmap 2.0.0", "indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime", "toml_datetime",
"winnow", "winnow",
] ]
@ -8459,6 +8428,15 @@ dependencies = [
"tree-sitter", "tree-sitter",
] ]
[[package]]
name = "tree-sitter-gleam"
version = "0.34.0"
source = "git+https://github.com/gleam-lang/tree-sitter-gleam?rev=58b7cac8fc14c92b0677c542610d8738c373fa81#58b7cac8fc14c92b0677c542610d8738c373fa81"
dependencies = [
"cc",
"tree-sitter",
]
[[package]] [[package]]
name = "tree-sitter-glsl" name = "tree-sitter-glsl"
version = "0.1.4" version = "0.1.4"
@ -8672,6 +8650,15 @@ dependencies = [
"tree-sitter", "tree-sitter",
] ]
[[package]]
name = "tree-sitter-zig"
version = "0.0.1"
source = "git+https://github.com/maxxnino/tree-sitter-zig?rev=0d08703e4c3f426ec61695d7617415fff97029bd#0d08703e4c3f426ec61695d7617415fff97029bd"
dependencies = [
"cc",
"tree-sitter",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.4" version = "0.2.4"
@ -9043,10 +9030,12 @@ dependencies = [
[[package]] [[package]]
name = "vte" name = "vte"
version = "0.11.1" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
dependencies = [ dependencies = [
"bitflags 2.4.1",
"cursor-icon",
"log", "log",
"serde", "serde",
"utf8parse", "utf8parse",
@ -9620,15 +9609,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "0.5.1"
@ -9671,7 +9651,6 @@ dependencies = [
"client", "client",
"collab_ui", "collab_ui",
"collections", "collections",
"color",
"command_palette", "command_palette",
"copilot", "copilot",
"copilot_ui", "copilot_ui",
@ -9738,7 +9717,7 @@ dependencies = [
"theme_selector", "theme_selector",
"thiserror", "thiserror",
"tiny_http", "tiny_http",
"toml 0.5.11", "toml",
"tree-sitter", "tree-sitter",
"tree-sitter-bash", "tree-sitter-bash",
"tree-sitter-c", "tree-sitter-c",
@ -9747,6 +9726,7 @@ dependencies = [
"tree-sitter-elixir", "tree-sitter-elixir",
"tree-sitter-elm", "tree-sitter-elm",
"tree-sitter-embedded-template", "tree-sitter-embedded-template",
"tree-sitter-gleam",
"tree-sitter-glsl", "tree-sitter-glsl",
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-haskell", "tree-sitter-haskell",
@ -9769,6 +9749,7 @@ dependencies = [
"tree-sitter-uiua", "tree-sitter-uiua",
"tree-sitter-vue", "tree-sitter-vue",
"tree-sitter-yaml", "tree-sitter-yaml",
"tree-sitter-zig",
"unindent", "unindent",
"url", "url",
"urlencoding", "urlencoding",

View file

@ -140,6 +140,7 @@ tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir"
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"} tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"}
tree-sitter-embedded-template = "0.20.0" tree-sitter-embedded-template = "0.20.0"
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" } tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
@ -161,18 +162,12 @@ tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", re
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa"} tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
[patch.crates-io] [patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "31c40449749c4263a91a43593831b82229049a4c" }
# wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" } # wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation-sys = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
debug = "limited" debug = "limited"

View file

@ -447,6 +447,10 @@
// //
"lsp": "elixir_ls" "lsp": "elixir_ls"
}, },
// Settings specific to our deno integration
"deno": {
"enable": false
},
// Different settings for specific languages. // Different settings for specific languages.
"languages": { "languages": {
"Plain Text": { "Plain Text": {

View file

@ -2959,6 +2959,7 @@ impl InlineAssistant {
cx.prompt( cx.prompt(
PromptLevel::Info, PromptLevel::Info,
prompt_text.as_str(), prompt_text.as_str(),
None,
&["Continue", "Cancel"], &["Continue", "Cancel"],
) )
})?; })?;

View file

@ -130,7 +130,8 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
} else { } else {
drop(cx.prompt( drop(cx.prompt(
gpui::PromptLevel::Info, gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.", "Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"], &["Ok"],
)); ));
} }

View file

@ -689,12 +689,7 @@ impl Client {
Ok(()) Ok(())
} }
Err(error) => { Err(error) => {
client.respond_with_error( client.respond_with_error(receipt, error.to_proto())?;
receipt,
proto::Error {
message: format!("{:?}", error),
},
)?;
Err(error) Err(error)
} }
} }

View file

@ -287,7 +287,7 @@ impl UserStore {
load_users.await?; load_users.await?;
// Users are fetched in parallel above and cached in call to get_users // Users are fetched in parallel above and cached in call to get_users
// No need to paralellize here // No need to parallelize here
let mut updated_contacts = Vec::new(); let mut updated_contacts = Vec::new();
let this = this let this = this
.upgrade() .upgrade()

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.40.1" version = "0.41.0"
publish = false publish = false
license = "AGPL-3.0-only" license = "AGPL-3.0-only"

View file

@ -169,6 +169,30 @@ impl Database {
self.run(body).await self.run(body).await
} }
pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let (tx, result) = self.with_weak_transaction(&f).await?;
match result {
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
return Err(error);
}
},
Err(error) => {
tx.rollback().await?;
return Err(error);
}
}
};
self.run(body).await
}
/// The same as room_transaction, but if you need to only optionally return a Room. /// The same as room_transaction, but if you need to only optionally return a Room.
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>> async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
where where
@ -284,6 +308,30 @@ impl Database {
Ok((tx, result)) Ok((tx, result))
} }
async fn with_weak_transaction<F, Fut, T>(
&self,
f: &F,
) -> Result<(DatabaseTransaction, Result<T>)>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let tx = self
.pool
.begin_with_config(Some(IsolationLevel::ReadCommitted), None)
.await?;
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> Result<T> async fn run<F, T>(&self, future: F) -> Result<T>
where where
F: Future<Output = Result<T>>, F: Future<Output = Result<T>>,
@ -303,13 +351,14 @@ impl Database {
} }
} }
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool { async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
// If the error is due to a failure to serialize concurrent transactions, then retry // If the error is due to a failure to serialize concurrent transactions, then retry
// this transaction after a delay. With each subsequent retry, double the delay duration. // this transaction after a delay. With each subsequent retry, double the delay duration.
// Also vary the delay randomly in order to ensure different database connections retry // Also vary the delay randomly in order to ensure different database connections retry
// at different times. // at different times.
if is_serialization_error(error) { const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
let base_delay = 4_u64 << prev_attempt_count.min(16); if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
let base_delay = SLEEPS[prev_attempt_count];
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0); let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!( log::info!(
"retrying transaction after serialization error. delay: {} ms.", "retrying transaction after serialization error. delay: {} ms.",
@ -456,9 +505,8 @@ pub struct NewUserResult {
/// The result of moving a channel. /// The result of moving a channel.
#[derive(Debug)] #[derive(Debug)]
pub struct MoveChannelResult { pub struct MoveChannelResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>, pub previous_participants: Vec<ChannelMember>,
pub participants_to_remove: HashSet<UserId>, pub descendent_ids: Vec<ChannelId>,
pub moved_channels: HashSet<ChannelId>,
} }
/// The result of renaming a channel. /// The result of renaming a channel.

View file

@ -1,5 +1,5 @@
use super::*; use super::*;
use rpc::proto::channel_member::Kind; use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
use sea_orm::TryGetableMany; use sea_orm::TryGetableMany;
impl Database { impl Database {
@ -19,11 +19,7 @@ impl Database {
#[cfg(test)] #[cfg(test)]
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> { pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
Ok(self Ok(self.create_channel(name, None, creator_id).await?.id)
.create_channel(name, None, creator_id)
.await?
.channel
.id)
} }
#[cfg(test)] #[cfg(test)]
@ -36,7 +32,6 @@ impl Database {
Ok(self Ok(self
.create_channel(name, Some(parent), creator_id) .create_channel(name, Some(parent), creator_id)
.await? .await?
.channel
.id) .id)
} }
@ -46,7 +41,7 @@ impl Database {
name: &str, name: &str,
parent_channel_id: Option<ChannelId>, parent_channel_id: Option<ChannelId>,
admin_id: UserId, admin_id: UserId,
) -> Result<CreateChannelResult> { ) -> Result<Channel> {
let name = Self::sanitize_channel_name(name)?; let name = Self::sanitize_channel_name(name)?;
self.transaction(move |tx| async move { self.transaction(move |tx| async move {
let mut parent = None; let mut parent = None;
@ -72,14 +67,7 @@ impl Database {
.insert(&*tx) .insert(&*tx)
.await?; .await?;
let participants_to_update; if parent.is_none() {
if let Some(parent) = &parent {
participants_to_update = self
.participants_to_notify_for_channel_change(parent, &*tx)
.await?;
} else {
participants_to_update = vec![];
channel_member::ActiveModel { channel_member::ActiveModel {
id: ActiveValue::NotSet, id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel.id), channel_id: ActiveValue::Set(channel.id),
@ -89,12 +77,9 @@ impl Database {
} }
.insert(&*tx) .insert(&*tx)
.await?; .await?;
}; }
Ok(CreateChannelResult { Ok(Channel::from_model(channel, ChannelRole::Admin))
channel: Channel::from_model(channel, ChannelRole::Admin),
participants_to_update,
})
}) })
.await .await
} }
@ -166,7 +151,7 @@ impl Database {
} }
if role.is_none() || role == Some(ChannelRole::Banned) { if role.is_none() || role == Some(ChannelRole::Banned) {
Err(anyhow!("not allowed"))? Err(ErrorCode::Forbidden.anyhow())?
} }
let role = role.unwrap(); let role = role.unwrap();
@ -718,6 +703,19 @@ impl Database {
}) })
} }
pub async fn new_participants_to_notify(
&self,
parent_channel_id: ChannelId,
) -> Result<Vec<(UserId, ChannelsForUser)>> {
self.weak_transaction(|tx| async move {
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
self.participants_to_notify_for_channel_change(&parent_channel, &*tx)
.await
})
.await
}
// TODO: this is very expensive, and we should rethink
async fn participants_to_notify_for_channel_change( async fn participants_to_notify_for_channel_change(
&self, &self,
new_parent: &channel::Model, new_parent: &channel::Model,
@ -1201,7 +1199,7 @@ impl Database {
Ok(channel::Entity::find_by_id(channel_id) Ok(channel::Entity::find_by_id(channel_id)
.one(&*tx) .one(&*tx)
.await? .await?
.ok_or_else(|| anyhow!("no such channel"))?) .ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
} }
pub(crate) async fn get_or_create_channel_room( pub(crate) async fn get_or_create_channel_room(
@ -1219,7 +1217,9 @@ impl Database {
let room_id = if let Some(room) = room { let room_id = if let Some(room) = room {
if let Some(env) = room.environment { if let Some(env) = room.environment {
if &env != environment { if &env != environment {
Err(anyhow!("must join using the {} release", env))?; Err(ErrorCode::WrongReleaseChannel
.with_tag("required", &env)
.anyhow())?;
} }
} }
room.id room.id
@ -1285,7 +1285,7 @@ impl Database {
let mut model = channel.into_active_model(); let mut model = channel.into_active_model();
model.parent_path = ActiveValue::Set(new_parent_path); model.parent_path = ActiveValue::Set(new_parent_path);
let channel = model.update(&*tx).await?; model.update(&*tx).await?;
if new_parent_channel.is_none() { if new_parent_channel.is_none() {
channel_member::ActiveModel { channel_member::ActiveModel {
@ -1312,34 +1312,9 @@ impl Database {
.all(&*tx) .all(&*tx)
.await?; .await?;
let participants_to_update: HashMap<_, _> = self
.participants_to_notify_for_channel_change(
new_parent_channel.as_ref().unwrap_or(&channel),
&*tx,
)
.await?
.into_iter()
.collect();
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
}
}
}
Ok(Some(MoveChannelResult { Ok(Some(MoveChannelResult {
participants_to_remove, previous_participants,
participants_to_update, descendent_ids,
moved_channels,
})) }))
}) })
.await .await

View file

@ -15,22 +15,18 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) { async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = new_test_user(db, "user@example.com").await; let user = new_test_user(db, "user@example.com").await;
let result = db.create_channel("channel", None, 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; let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat( db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
result.channel.id, .await
rpc::ConnectionId { owner_id, id: 0 }, .unwrap();
user,
)
.await
.unwrap();
let mut all_messages = Vec::new(); let mut all_messages = Vec::new();
for i in 0..10 { for i in 0..10 {
all_messages.push( all_messages.push(
db.create_channel_message( db.create_channel_message(
result.channel.id, channel.id,
user, user,
&i.to_string(), &i.to_string(),
&[], &[],
@ -45,7 +41,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
} }
let messages = db let messages = db
.get_channel_messages(result.channel.id, user, 3, None) .get_channel_messages(channel.id, user, 3, None)
.await .await
.unwrap() .unwrap()
.into_iter() .into_iter()
@ -55,7 +51,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
let messages = db let messages = db
.get_channel_messages( .get_channel_messages(
result.channel.id, channel.id,
user, user,
4, 4,
Some(MessageId::from_proto(all_messages[6])), Some(MessageId::from_proto(all_messages[6])),
@ -100,7 +96,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.await .await
.unwrap(); .unwrap();
// As user A, create messages that re-use the same nonces. The requests // As user A, create messages that reuse the same nonces. The requests
// succeed, but return the same ids. // succeed, but return the same ids.
let id1 = db let id1 = db
.create_channel_message( .create_channel_message(
@ -366,12 +362,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
let user_b = new_test_user(db, "user_b@example.com").await; let user_b = new_test_user(db, "user_b@example.com").await;
let user_c = new_test_user(db, "user_c@example.com").await; let user_c = new_test_user(db, "user_c@example.com").await;
let channel = db let channel = db.create_channel("channel", None, user_a).await.unwrap().id;
.create_channel("channel", None, user_a)
.await
.unwrap()
.channel
.id;
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member) db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await .await
.unwrap(); .unwrap();

View file

@ -3,14 +3,13 @@ mod connection_pool;
use crate::{ use crate::{
auth::{self, Impersonator}, auth::{self, Impersonator},
db::{ db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult, RemoveChannelMemberResult, RenameChannelResult, RespondToChannelInvite, RoomId, ServerId,
RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult, SetChannelVisibilityResult, User, UserId,
User, UserId,
}, },
executor::Executor, executor::Executor,
AppState, Result, AppState, Error, Result,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use async_tungstenite::tungstenite::{ use async_tungstenite::tungstenite::{
@ -44,7 +43,7 @@ use rpc::{
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage, ShareProject, UpdateChannelBufferCollaborators, RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
}, },
Connection, ConnectionId, Peer, Receipt, TypedEnvelope, Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
}; };
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use std::{ use std::{
@ -543,12 +542,11 @@ impl Server {
} }
} }
Err(error) => { Err(error) => {
peer.respond_with_error( let proto_err = match &error {
receipt, Error::Internal(err) => err.to_proto(),
proto::Error { _ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
message: error.to_string(), };
}, peer.respond_with_error(receipt, proto_err)?;
)?;
Err(error) Err(error)
} }
} }
@ -2302,10 +2300,7 @@ async fn create_channel(
let db = session.db().await; let db = session.db().await;
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id)); let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let CreateChannelResult { let channel = db
channel,
participants_to_update,
} = db
.create_channel(&request.name, parent_id, session.user_id) .create_channel(&request.name, parent_id, session.user_id)
.await?; .await?;
@ -2314,6 +2309,13 @@ async fn create_channel(
parent_id: request.parent_id, parent_id: request.parent_id,
})?; })?;
let participants_to_update;
if let Some(parent) = parent_id {
participants_to_update = db.new_participants_to_notify(parent).await?;
} else {
participants_to_update = vec![];
}
let connection_pool = session.connection_pool().await; let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update { for (user_id, channels) in participants_to_update {
let update = build_channels_update(channels, vec![]); let update = build_channels_update(channels, vec![]);
@ -2573,44 +2575,56 @@ async fn move_channel(
.move_channel(channel_id, to, session.user_id) .move_channel(channel_id, to, session.user_id)
.await?; .await?;
notify_channel_moved(result, session).await?; if let Some(result) = result {
let participants_to_update: HashMap<_, _> = session
.db()
.await
.new_participants_to_notify(to.unwrap_or(channel_id))
.await?
.into_iter()
.collect();
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in result.descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in result.previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
}
}
}
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
}
response.send(Ack {})?; response.send(Ack {})?;
Ok(()) Ok(())
} }
async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
let Some(MoveChannelResult {
participants_to_remove,
participants_to_update,
moved_channels,
}) = result
else {
return Ok(());
};
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
Ok(())
}
/// Get the list of channel members /// Get the list of channel members
async fn get_channel_members( async fn get_channel_members(
request: proto::GetChannelMembers, request: proto::GetChannelMembers,

View file

@ -343,9 +343,11 @@ impl ChatPanel {
this.pt_3().child( this.pt_3().child(
h_flex() h_flex()
.text_ui_sm() .text_ui_sm()
.child(div().absolute().child( .child(
Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()), div().absolute().child(
)) Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
),
)
.child( .child(
div() div()
.pl(cx.rem_size() + px(6.0)) .pl(cx.rem_size() + px(6.0))

View file

@ -22,7 +22,10 @@ use gpui::{
}; };
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev}; use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project}; use project::{Fs, Project};
use rpc::proto::{self, PeerId}; use rpc::{
proto::{self, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -35,7 +38,7 @@ use ui::{
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
notifications::{NotifyResultExt, NotifyTaskExt}, notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
Workspace, Workspace,
}; };
@ -879,7 +882,7 @@ impl CollabPanel {
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone(); let app_state = workspace.app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx) workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx); .detach_and_prompt_err("Failed to join project", cx, |_, _| None);
}) })
.ok(); .ok();
})) }))
@ -1017,7 +1020,12 @@ impl CollabPanel {
) )
}) })
}) })
.detach_and_notify_err(cx) .detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
match e.error_code() {
ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
_ => None,
}
})
}), }),
) )
} else if role == proto::ChannelRole::Member { } else if role == proto::ChannelRole::Member {
@ -1038,7 +1046,7 @@ impl CollabPanel {
) )
}) })
}) })
.detach_and_notify_err(cx) .detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
}), }),
) )
} else { } else {
@ -1258,7 +1266,11 @@ impl CollabPanel {
app_state, app_state,
cx, cx,
) )
.detach_and_log_err(cx); .detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
);
} }
} }
ListEntry::ParticipantScreen { peer_id, .. } => { ListEntry::ParticipantScreen { peer_id, .. } => {
@ -1432,7 +1444,7 @@ impl CollabPanel {
fn leave_call(cx: &mut WindowContext) { fn leave_call(cx: &mut WindowContext) {
ActiveCall::global(cx) ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx)) .update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx); .detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
} }
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) { fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
@ -1534,11 +1546,11 @@ impl CollabPanel {
cx: &mut ViewContext<CollabPanel>, cx: &mut ViewContext<CollabPanel>,
) { ) {
if let Some(clipboard) = self.channel_clipboard.take() { if let Some(clipboard) = self.channel_clipboard.take() {
self.channel_store.update(cx, |channel_store, cx| { self.channel_store
channel_store .update(cx, |channel_store, cx| {
.move_channel(clipboard.channel_id, Some(to_channel_id), cx) channel_store.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
.detach_and_log_err(cx) })
}) .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
} }
} }
@ -1610,7 +1622,12 @@ impl CollabPanel {
"Are you sure you want to remove the channel \"{}\"?", "Are you sure you want to remove the channel \"{}\"?",
channel.name channel.name
); );
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
if answer.await? == 0 { if answer.await? == 0 {
channel_store channel_store
@ -1631,7 +1648,12 @@ impl CollabPanel {
"Are you sure you want to remove \"{}\" from your contacts?", "Are you sure you want to remove \"{}\" from your contacts?",
github_login github_login
); );
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
if answer.await? == 0 { if answer.await? == 0 {
user_store user_store
@ -1641,7 +1663,7 @@ impl CollabPanel {
} }
anyhow::Ok(()) anyhow::Ok(())
}) })
.detach_and_log_err(cx); .detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
} }
fn respond_to_contact_request( fn respond_to_contact_request(
@ -1654,7 +1676,7 @@ impl CollabPanel {
.update(cx, |store, cx| { .update(cx, |store, cx| {
store.respond_to_contact_request(user_id, accept, cx) store.respond_to_contact_request(user_id, accept, cx)
}) })
.detach_and_log_err(cx); .detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
} }
fn respond_to_channel_invite( fn respond_to_channel_invite(
@ -1675,7 +1697,7 @@ impl CollabPanel {
.update(cx, |call, cx| { .update(cx, |call, cx| {
call.invite(recipient_user_id, Some(self.project.clone()), cx) call.invite(recipient_user_id, Some(self.project.clone()), cx)
}) })
.detach_and_log_err(cx); .detach_and_prompt_err("Call failed", cx, |_, _| None);
} }
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) { fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
@ -1691,7 +1713,7 @@ impl CollabPanel {
Some(handle), Some(handle),
cx, cx,
) )
.detach_and_log_err(cx) .detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
} }
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) { fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
@ -1704,7 +1726,7 @@ impl CollabPanel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel panel
.select_channel(channel_id, None, cx) .select_channel(channel_id, None, cx)
.detach_and_log_err(cx); .detach_and_notify_err(cx);
}); });
} }
}); });
@ -1981,7 +2003,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, None, cx) channel_store.move_channel(dragged_channel.id, None, cx)
}) })
.detach_and_log_err(cx) .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
})) }))
}) })
} }
@ -2257,7 +2279,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| { .update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
}) })
.detach_and_log_err(cx) .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
})) }))
.child( .child(
ListItem::new(channel_id as usize) ListItem::new(channel_id as usize)

View file

@ -14,7 +14,7 @@ use rpc::proto::channel_member;
use std::sync::Arc; use std::sync::Arc;
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing}; use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{notifications::NotifyTaskExt, ModalView}; use workspace::{notifications::DetachAndPromptErr, ModalView};
actions!( actions!(
channel_modal, channel_modal,
@ -498,7 +498,7 @@ impl ChannelModalDelegate {
cx.notify(); cx.notify();
}) })
}) })
.detach_and_notify_err(cx); .detach_and_prompt_err("Failed to update role", cx, |_, _| None);
Some(()) Some(())
} }
@ -530,7 +530,7 @@ impl ChannelModalDelegate {
cx.notify(); cx.notify();
}) })
}) })
.detach_and_notify_err(cx); .detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
Some(()) Some(())
} }
@ -556,7 +556,7 @@ impl ChannelModalDelegate {
cx.notify(); cx.notify();
}) })
}) })
.detach_and_notify_err(cx); .detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
} }
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) { fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {

View file

@ -542,7 +542,9 @@ impl CollabTitlebarItem {
})? })?
.clone(); .clone();
Some(Avatar::new(follower.avatar_uri.clone())) Some(div().mt(-px(4.)).child(
Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
))
}, },
)) ))
.children(if extra_count > 0 { .children(if extra_count > 0 {

View file

@ -5,7 +5,6 @@ edition = "2021"
publish = false publish = false
license = "GPL-3.0-only" license = "GPL-3.0-only"
[features] [features]
default = [] default = []
stories = ["dep:itertools", "dep:story"] stories = ["dep:itertools", "dep:story"]
@ -15,20 +14,6 @@ path = "src/color.rs"
doctest = true doctest = true
[dependencies] [dependencies]
# TODO: Clean up dependencies
anyhow.workspace = true
fs = { path = "../fs" }
indexmap = "1.6.2"
parking_lot.workspace = true
refineable.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings = { path = "../settings" }
story = { path = "../story", optional = true } story = { path = "../story", optional = true }
toml.workspace = true
uuid.workspace = true
util = { path = "../util" }
itertools = { version = "0.11.0", optional = true } itertools = { version = "0.11.0", optional = true }
palette = "0.7.3" palette = "0.7.3"

View file

@ -59,10 +59,7 @@ pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
let hex = match hex.len() { let hex = match hex.len() {
3 => hex 3 => hex.chars().map(|c| c.to_string().repeat(2)).collect(),
.chars()
.map(|c| c.to_string().repeat(2))
.collect::<String>(),
4 => { 4 => {
let (rgb, alpha) = hex.split_at(3); let (rgb, alpha) = hex.split_at(3);
let rgb = rgb let rgb = rgb
@ -80,14 +77,12 @@ pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
let hex_val = let hex_val =
u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?; u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0; Ok(RGBAColor {
let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0; r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
let a = (hex_val & 0xFF) as f32 / 255.0; b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
a: (hex_val & 0xFF) as f32 / 255.0,
let color = RGBAColor { r, g, b, a }; })
Ok(color)
} }
// These derives implement to and from palette's color types. // These derives implement to and from palette's color types.
@ -128,8 +123,7 @@ where
Rgb<S, f32>: FromColorUnclamped<Srgb>, Rgb<S, f32>: FromColorUnclamped<Srgb>,
{ {
fn from_color_unclamped(color: RGBAColor) -> Self { fn from_color_unclamped(color: RGBAColor) -> Self {
let srgb = Srgb::new(color.r, color.g, color.b); Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
Self::from_color_unclamped(srgb)
} }
} }

View file

@ -88,15 +88,15 @@ impl CopilotCodeVerification {
let connect_button_label = if connect_clicked { let connect_button_label = if connect_clicked {
"Waiting for connection..." "Waiting for connection..."
} else { } else {
"Connect to Github" "Connect to GitHub"
}; };
v_flex() v_flex()
.flex_1() .flex_1()
.gap_2() .gap_2()
.items_center() .items_center()
.child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large)) .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
.child( .child(
Label::new("Using Copilot requires an active subscription on Github.") Label::new("Using Copilot requires an active subscription on GitHub.")
.color(Color::Muted), .color(Color::Muted),
) )
.child(Self::render_device_code(data, cx)) .child(Self::render_device_code(data, cx))
@ -139,7 +139,7 @@ impl CopilotCodeVerification {
"You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.", "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
).color(Color::Warning)) ).color(Color::Warning))
.child( .child(
Button::new("copilot-subscribe-button", "Subscribe on Github") Button::new("copilot-subscribe-button", "Subscribe on GitHub")
.full_width() .full_width()
.on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)), .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
) )

View file

@ -13,12 +13,6 @@ pub struct SelectPrevious {
pub replace_newest: bool, pub replace_newest: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine { pub struct SelectToBeginningOfLine {
#[serde(default)] #[serde(default)]
@ -81,7 +75,6 @@ impl_actions!(
[ [
SelectNext, SelectNext,
SelectPrevious, SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine, SelectToBeginningOfLine,
MovePageUp, MovePageUp,
MovePageDown, MovePageDown,
@ -128,6 +121,7 @@ gpui::actions!(
DeleteToNextWordEnd, DeleteToNextWordEnd,
DeleteToPreviousSubwordStart, DeleteToPreviousSubwordStart,
DeleteToPreviousWordStart, DeleteToPreviousWordStart,
DisplayCursorNames,
DuplicateLine, DuplicateLine,
ExpandMacroRecursively, ExpandMacroRecursively,
FindAllReferences, FindAllReferences,
@ -185,6 +179,7 @@ gpui::actions!(
ScrollCursorCenter, ScrollCursorCenter,
ScrollCursorTop, ScrollCursorTop,
SelectAll, SelectAll,
SelectAllMatches,
SelectDown, SelectDown,
SelectLargerSyntaxNode, SelectLargerSyntaxNode,
SelectLeft, SelectLeft,
@ -214,6 +209,5 @@ gpui::actions!(
Undo, Undo,
UndoSelection, UndoSelection,
UnfoldLines, UnfoldLines,
DisplayCursorNames
] ]
); );

View file

@ -2502,34 +2502,43 @@ impl Editor {
) )
}); });
// Comment extension on newline is allowed only for cursor selections // Comment extension on newline is allowed only for cursor selections
let comment_delimiter = language.line_comment_prefix().filter(|_| { let comment_delimiter = language.line_comment_prefixes().filter(|_| {
let is_comment_extension_enabled = let is_comment_extension_enabled =
multi_buffer.settings_at(0, cx).extend_comment_on_newline; multi_buffer.settings_at(0, cx).extend_comment_on_newline;
is_cursor && is_comment_extension_enabled is_cursor && is_comment_extension_enabled
}); });
let comment_delimiter = if let Some(delimiter) = comment_delimiter { let get_comment_delimiter = |delimiters: &[Arc<str>]| {
buffer let max_len_of_delimiter =
.buffer_line_for_row(start_point.row) delimiters.iter().map(|delimiter| delimiter.len()).max()?;
.is_some_and(|(snapshot, range)| { let (snapshot, range) =
let mut index_of_first_non_whitespace = 0; buffer.buffer_line_for_row(start_point.row)?;
let line_starts_with_comment = snapshot
.chars_for_range(range) let mut index_of_first_non_whitespace = 0;
.skip_while(|c| { let comment_candidate = snapshot
let should_skip = c.is_whitespace(); .chars_for_range(range)
if should_skip { .skip_while(|c| {
index_of_first_non_whitespace += 1; let should_skip = c.is_whitespace();
} if should_skip {
should_skip index_of_first_non_whitespace += 1;
}) }
.take(delimiter.len()) should_skip
.eq(delimiter.chars());
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + delimiter.len()
<= start_point.column as usize;
line_starts_with_comment
&& cursor_is_placed_after_comment_marker
}) })
.then(|| delimiter.clone()) .take(max_len_of_delimiter)
.collect::<String>();
let comment_prefix = delimiters.iter().find(|comment_prefix| {
comment_candidate.starts_with(comment_prefix.as_ref())
})?;
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + comment_prefix.len()
<= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
Some(comment_prefix.clone())
} else {
None
}
};
let comment_delimiter = if let Some(delimiters) = comment_delimiter {
get_comment_delimiter(delimiters)
} else { } else {
None None
}; };
@ -6113,6 +6122,7 @@ impl Editor {
|| (!movement::is_inside_word(&display_map, display_range.start) || (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end)) && !movement::is_inside_word(&display_map, display_range.end))
{ {
// TODO: This is n^2, because we might check all the selections
if selections if selections
.iter() .iter()
.find(|selection| selection.range().overlaps(&offset_range)) .find(|selection| selection.range().overlaps(&offset_range))
@ -6222,25 +6232,76 @@ impl Editor {
pub fn select_all_matches( pub fn select_all_matches(
&mut self, &mut self,
action: &SelectAllMatches, _action: &SelectAllMatches,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Result<()> { ) -> Result<()> {
self.push_to_selection_history(); self.push_to_selection_history();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
loop { self.select_next_match_internal(&display_map, false, None, cx)?;
self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?; let Some(select_next_state) = self.select_next_state.as_mut() else {
return Ok(());
};
if select_next_state.done {
return Ok(());
}
if self let mut new_selections = self.selections.all::<usize>(cx);
.select_next_state
.as_ref() let buffer = &display_map.buffer_snapshot;
.map(|selection_state| selection_state.done) let query_matches = select_next_state
.unwrap_or(true) .query
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()));
for query_match in query_matches {
let query_match = query_match.unwrap(); // can only fail due to I/O
let offset_range = query_match.start()..query_match.end();
let display_range = offset_range.start.to_display_point(&display_map)
..offset_range.end.to_display_point(&display_map);
if !select_next_state.wordwise
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{ {
break; self.selections.change_with(cx, |selections| {
new_selections.push(Selection {
id: selections.new_selection_id(),
start: offset_range.start,
end: offset_range.end,
reversed: false,
goal: SelectionGoal::None,
});
});
} }
} }
new_selections.sort_by_key(|selection| selection.start);
let mut ix = 0;
while ix + 1 < new_selections.len() {
let current_selection = &new_selections[ix];
let next_selection = &new_selections[ix + 1];
if current_selection.range().overlaps(&next_selection.range()) {
if current_selection.id < next_selection.id {
new_selections.remove(ix + 1);
} else {
new_selections.remove(ix);
}
} else {
ix += 1;
}
}
select_next_state.done = true;
self.unfold_ranges(
new_selections.iter().map(|selection| selection.range()),
false,
false,
cx,
);
self.change_selections(Some(Autoscroll::fit()), cx, |selections| {
selections.select(new_selections)
});
Ok(()) Ok(())
} }
@ -6509,7 +6570,10 @@ impl Editor {
} }
// If the language has line comments, toggle those. // If the language has line comments, toggle those.
if let Some(full_comment_prefix) = language.line_comment_prefix() { if let Some(full_comment_prefix) = language
.line_comment_prefixes()
.and_then(|prefixes| prefixes.first())
{
// Split the comment prefix's trailing whitespace into a separate string, // Split the comment prefix's trailing whitespace into a separate string,
// as that portion won't be used for detecting if a line is a comment. // as that portion won't be used for detecting if a line is a comment.
let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix = full_comment_prefix.trim_end_matches(' ');
@ -6517,7 +6581,7 @@ impl Editor {
let mut all_selection_lines_are_comments = true; let mut all_selection_lines_are_comments = true;
for row in start_row..=end_row { for row in start_row..=end_row {
if snapshot.is_line_blank(row) && start_row < end_row { if start_row < end_row && snapshot.is_line_blank(row) {
continue; continue;
} }

View file

@ -1942,7 +1942,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
line_comment: Some("//".into()), line_comments: vec!["//".into()],
..LanguageConfig::default() ..LanguageConfig::default()
}, },
None, None,
@ -3820,6 +3820,18 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
} }
#[gpui::test]
async fn test_select_all_matches(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_all_matches(&SelectAllMatches::default(), cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
#[gpui::test] #[gpui::test]
async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) { async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
@ -5724,7 +5736,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
line_comment: Some("// ".into()), line_comments: vec!["// ".into()],
..Default::default() ..Default::default()
}, },
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
@ -5826,7 +5838,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
line_comment: Some("// ".into()), line_comments: vec!["// ".into()],
..Default::default() ..Default::default()
}, },
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
@ -5981,7 +5993,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
let javascript_language = Arc::new(Language::new( let javascript_language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
name: "JavaScript".into(), name: "JavaScript".into(),
line_comment: Some("// ".into()), line_comments: vec!["// ".into()],
..Default::default() ..Default::default()
}, },
Some(tree_sitter_typescript::language_tsx()), Some(tree_sitter_typescript::language_tsx()),

View file

@ -567,7 +567,7 @@ impl EditorElement {
cx, cx,
); );
hover_at(editor, Some(point), cx); hover_at(editor, Some(point), cx);
Self::update_visible_cursor(editor, point, cx); Self::update_visible_cursor(editor, point, position_map, cx);
} }
None => { None => {
update_inlay_link_and_hover_points( update_inlay_link_and_hover_points(
@ -592,9 +592,10 @@ impl EditorElement {
fn update_visible_cursor( fn update_visible_cursor(
editor: &mut Editor, editor: &mut Editor,
point: DisplayPoint, point: DisplayPoint,
position_map: &PositionMap,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
let snapshot = editor.snapshot(cx); let snapshot = &position_map.snapshot;
let Some(hub) = editor.collaboration_hub() else { let Some(hub) = editor.collaboration_hub() else {
return; return;
}; };
@ -3581,7 +3582,7 @@ mod tests {
); );
// multi-buffer support // multi-buffer support
// in DisplayPoint co-ordinates, this is what we're dealing with: // in DisplayPoint coordinates, this is what we're dealing with:
// 0: [[file // 0: [[file
// 1: header]] // 1: header]]
// 2: aaaaaa // 2: aaaaaa

View file

@ -1971,7 +1971,7 @@ pub mod tests {
assert_eq!( assert_eq!(
lsp_request_count.load(Ordering::Relaxed), lsp_request_count.load(Ordering::Relaxed),
3, 3,
"Should query for new hints when they got reenabled" "Should query for new hints when they got re-enabled"
); );
assert_eq!( assert_eq!(
vec![ vec![
@ -1980,7 +1980,7 @@ pub mod tests {
"type hint".to_string(), "type hint".to_string(),
], ],
cached_hint_labels(editor), cached_hint_labels(editor),
"Should get its cached hints fully repopulated after the hints got reenabled" "Should get its cached hints fully repopulated after the hints got re-enabled"
); );
assert_eq!( assert_eq!(
vec!["parameter hint".to_string()], vec!["parameter hint".to_string()],
@ -1990,11 +1990,11 @@ pub mod tests {
let inlay_cache = editor.inlay_hint_cache(); let inlay_cache = editor.inlay_hint_cache();
assert_eq!( assert_eq!(
inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
"Cache should update editor settings when hints got reenabled" "Cache should update editor settings when hints got re-enabled"
); );
assert_eq!( assert_eq!(
inlay_cache.version, edits_made, inlay_cache.version, edits_made,
"Cache should update its version after hints got reenabled" "Cache should update its version after hints got re-enabled"
); );
}); });
@ -2736,7 +2736,7 @@ pub mod tests {
assert_eq!(expected_hints, cached_hint_labels(editor), assert_eq!(expected_hints, cached_hint_labels(editor),
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scrolled buffer");
}); });
editor_edited.store(true, Ordering::Release); editor_edited.store(true, Ordering::Release);
@ -2762,7 +2762,7 @@ pub mod tests {
assert_eq!( assert_eq!(
expected_hints, expected_hints,
cached_hint_labels(editor), cached_hint_labels(editor),
"After multibuffer edit, editor gets scolled back to the last selection; \ "After multibuffer edit, editor gets scrolled back to the last selection; \
all hints should be invalidated and required for all of its visible excerpts" all hints should be invalidated and required for all of its visible excerpts"
); );
assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx));

View file

@ -15,47 +15,53 @@ actions!(
CopySystemSpecsIntoClipboard, CopySystemSpecsIntoClipboard,
FileBugReport, FileBugReport,
RequestFeature, RequestFeature,
OpenZedCommunityRepo OpenZedRepo
] ]
); );
pub fn init(cx: &mut AppContext) { const fn zed_repo_url() -> &'static str {
// TODO: a way to combine these two into one? "https://github.com/zed-industries/zed"
cx.observe_new_views(feedback_modal::FeedbackModal::register) }
.detach();
cx.observe_new_views(|workspace: &mut Workspace, _| { const fn request_feature_url() -> &'static str {
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"
}
fn file_bug_report_url(specs: &SystemSpecs) -> String {
format!(
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&specs.to_string())
)
}
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, cx| {
feedback_modal::FeedbackModal::register(workspace, cx);
workspace workspace
.register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| { .register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| {
let specs = SystemSpecs::new(&cx).to_string(); let specs = SystemSpecs::new(&cx).to_string();
let prompt = cx.prompt( let prompt = cx.prompt(
PromptLevel::Info, PromptLevel::Info,
&format!("Copied into clipboard:\n\n{specs}"), "Copied into clipboard",
&["OK"], Some(&specs),
); &["OK"],
cx.spawn(|_, _cx| async move { );
prompt.await.ok(); cx.spawn(|_, _cx| async move {
}) prompt.await.ok();
.detach();
let item = ClipboardItem::new(specs.clone());
cx.write_to_clipboard(item);
}) })
.detach();
cx.write_to_clipboard(ClipboardItem::new(specs.clone()));
})
.register_action(|_, _: &RequestFeature, cx| { .register_action(|_, _: &RequestFeature, cx| {
let url = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml"; cx.open_url(request_feature_url());
cx.open_url(url);
}) })
.register_action(move |_, _: &FileBugReport, cx| { .register_action(move |_, _: &FileBugReport, cx| {
let url = format!( cx.open_url(&file_bug_report_url(&SystemSpecs::new(&cx)));
"https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&SystemSpecs::new(&cx).to_string())
);
cx.open_url(&url);
}) })
.register_action(move |_, _: &OpenZedCommunityRepo, cx| { .register_action(move |_, _: &OpenZedRepo, cx| {
let url = "https://github.com/zed-industries/zed"; cx.open_url(zed_repo_url());
cx.open_url(&url); });
});
}) })
.detach(); .detach();
} }

View file

@ -19,7 +19,7 @@ use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::{ModalView, Toast, Workspace}; use workspace::{ModalView, Toast, Workspace};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo}; use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
// For UI testing purposes // For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true; const SEND_SUCCESS_IN_DEV_MODE: bool = true;
@ -97,7 +97,7 @@ impl ModalView for FeedbackModal {
return true; return true;
} }
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]); let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", None, &["Yes", "No"]);
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
if answer.await.ok() == Some(0) { if answer.await.ok() == Some(0) {
@ -222,6 +222,7 @@ impl FeedbackModal {
let answer = cx.prompt( let answer = cx.prompt(
PromptLevel::Info, PromptLevel::Info,
"Ready to submit your feedback?", "Ready to submit your feedback?",
None,
&["Yes, Submit!", "No"], &["Yes, Submit!", "No"],
); );
let client = cx.global::<Arc<Client>>().clone(); let client = cx.global::<Arc<Client>>().clone();
@ -255,6 +256,7 @@ impl FeedbackModal {
let prompt = cx.prompt( let prompt = cx.prompt(
PromptLevel::Critical, PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT, FEEDBACK_SUBMISSION_ERROR_TEXT,
None,
&["OK"], &["OK"],
); );
cx.spawn(|_, _cx| async move { cx.spawn(|_, _cx| async move {
@ -417,8 +419,7 @@ impl Render for FeedbackModal {
"Submit" "Submit"
}; };
let open_community_repo = let open_zed_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedRepo)));
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
v_flex() v_flex()
.elevation_3(cx) .elevation_3(cx)
@ -485,12 +486,12 @@ impl Render for FeedbackModal {
.justify_between() .justify_between()
.gap_1() .gap_1()
.child( .child(
Button::new("community_repository", "Community Repository") Button::new("zed_repository", "Zed Repository")
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.icon(IconName::ExternalLink) .icon(IconName::ExternalLink)
.icon_position(IconPosition::End) .icon_position(IconPosition::End)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.on_click(open_community_repo), .on_click(open_zed_repo),
) )
.child( .child(
h_flex() h_flex()

View file

@ -11,6 +11,7 @@ use gpui::{
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{ use std::{
cmp,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::{
atomic::{self, AtomicBool}, atomic::{self, AtomicBool},
@ -143,16 +144,51 @@ pub struct FileFinderDelegate {
history_items: Vec<FoundPath>, history_items: Vec<FoundPath>,
} }
/// Use a custom ordering for file finder: the regular one
/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
///
/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
/// as the files are shown in the project panel lists.
#[derive(Debug, Clone, PartialEq, Eq)]
struct ProjectPanelOrdMatch(PathMatch);
impl Ord for ProjectPanelOrdMatch {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
impl PartialOrd for ProjectPanelOrdMatch {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(
self.0
.score
.partial_cmp(&other.0.score)
.unwrap_or(cmp::Ordering::Equal)
.then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
.then_with(|| {
other
.0
.distance_to_relative_ancestor
.cmp(&self.0.distance_to_relative_ancestor)
})
.then_with(|| self.0.path.cmp(&other.0.path).reverse()),
)
}
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Matches { struct Matches {
history: Vec<(FoundPath, Option<PathMatch>)>, history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
search: Vec<PathMatch>, search: Vec<ProjectPanelOrdMatch>,
} }
#[derive(Debug)] #[derive(Debug)]
enum Match<'a> { enum Match<'a> {
History(&'a FoundPath, Option<&'a PathMatch>), History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
Search(&'a PathMatch), Search(&'a ProjectPanelOrdMatch),
} }
impl Matches { impl Matches {
@ -176,45 +212,44 @@ impl Matches {
&mut self, &mut self,
history_items: &Vec<FoundPath>, history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>, query: &PathLikeWithPosition<FileSearchQuery>,
mut new_search_matches: Vec<PathMatch>, new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool, extend_old_matches: bool,
) { ) {
let matching_history_paths = matching_history_item_paths(history_items, query); let matching_history_paths = matching_history_item_paths(history_items, query);
new_search_matches let new_search_matches = new_search_matches
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
let history_items_to_show = history_items let history_items_to_show = history_items.iter().filter_map(|history_item| {
.iter() Some((
.filter_map(|history_item| { history_item.clone(),
Some(( Some(
history_item.clone(), matching_history_paths
Some( .get(&history_item.project.path)?
matching_history_paths .clone(),
.get(&history_item.project.path)? ),
.clone(), ))
), });
)) self.history.clear();
}) util::extend_sorted(
.collect::<Vec<_>>(); &mut self.history,
self.history = history_items_to_show; history_items_to_show,
100,
|(_, a), (_, b)| b.cmp(a),
);
if extend_old_matches { if extend_old_matches {
self.search self.search
.retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); .retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
util::extend_sorted(
&mut self.search,
new_search_matches.into_iter(),
100,
|a, b| b.cmp(a),
)
} else { } else {
self.search = new_search_matches; self.search.clear();
} }
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
} }
} }
fn matching_history_item_paths( fn matching_history_item_paths(
history_items: &Vec<FoundPath>, history_items: &Vec<FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>, query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, PathMatch> { ) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
let history_items_by_worktrees = history_items let history_items_by_worktrees = history_items
.iter() .iter()
.filter_map(|found_path| { .filter_map(|found_path| {
@ -257,7 +292,12 @@ fn matching_history_item_paths(
max_results, max_results,
) )
.into_iter() .into_iter()
.map(|path_match| (Arc::clone(&path_match.path), path_match)), .map(|path_match| {
(
Arc::clone(&path_match.path),
ProjectPanelOrdMatch(path_match),
)
}),
); );
} }
matching_history_paths matching_history_paths
@ -383,7 +423,9 @@ impl FileFinderDelegate {
&cancel_flag, &cancel_flag,
cx.background_executor().clone(), cx.background_executor().clone(),
) )
.await; .await
.into_iter()
.map(ProjectPanelOrdMatch);
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
picker picker
.update(&mut cx, |picker, cx| { .update(&mut cx, |picker, cx| {
@ -401,7 +443,7 @@ impl FileFinderDelegate {
search_id: usize, search_id: usize,
did_cancel: bool, did_cancel: bool,
query: PathLikeWithPosition<FileSearchQuery>, query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>, matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) { ) {
if search_id >= self.latest_search_id { if search_id >= self.latest_search_id {
@ -412,8 +454,12 @@ impl FileFinderDelegate {
.latest_search_query .latest_search_query
.as_ref() .as_ref()
.map(|query| query.path_like.path_query()); .map(|query| query.path_like.path_query());
self.matches self.matches.push_new_matches(
.push_new_matches(&self.history_items, &query, matches, extend_old_matches); &self.history_items,
&query,
matches.into_iter(),
extend_old_matches,
);
self.latest_search_query = Some(query); self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel; self.latest_search_did_cancel = did_cancel;
cx.notify(); cx.notify();
@ -471,12 +517,12 @@ impl FileFinderDelegate {
if let Some(found_path_match) = found_path_match { if let Some(found_path_match) = found_path_match {
path_match path_match
.positions .positions
.extend(found_path_match.positions.iter()) .extend(found_path_match.0.positions.iter())
} }
self.labels_for_path_match(&path_match) self.labels_for_path_match(&path_match)
} }
Match::Search(path_match) => self.labels_for_path_match(path_match), Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
}; };
if file_name_positions.is_empty() { if file_name_positions.is_empty() {
@ -556,14 +602,14 @@ impl FileFinderDelegate {
if let Some((worktree, relative_path)) = if let Some((worktree, relative_path)) =
project.find_local_worktree(query_path, cx) project.find_local_worktree(query_path, cx)
{ {
path_matches.push(PathMatch { path_matches.push(ProjectPanelOrdMatch(PathMatch {
score: 0.0, score: 1.0,
positions: Vec::new(), positions: Vec::new(),
worktree_id: worktree.read(cx).id().to_usize(), worktree_id: worktree.read(cx).id().to_usize(),
path: Arc::from(relative_path), path: Arc::from(relative_path),
path_prefix: "".into(), path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX, distance_to_relative_ancestor: usize::MAX,
}); }));
} }
}) })
.log_err(); .log_err();
@ -724,8 +770,8 @@ impl PickerDelegate for FileFinderDelegate {
Match::Search(m) => split_or_open( Match::Search(m) => split_or_open(
workspace, workspace,
ProjectPath { ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id), worktree_id: WorktreeId::from_usize(m.0.worktree_id),
path: m.path.clone(), path: m.0.path.clone(),
}, },
cx, cx,
), ),
@ -805,3 +851,101 @@ impl PickerDelegate for FileFinderDelegate {
) )
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_custom_project_search_ordering_in_file_finder() {
let mut file_finder_sorted_output = vec![
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
];
file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
assert_eq!(
file_finder_sorted_output,
vec![
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 1.0,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("c1.0")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("a0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
ProjectPanelOrdMatch(PathMatch {
score: 0.5,
positions: Vec::new(),
worktree_id: 0,
path: Arc::from(Path::new("b0.5")),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: 0,
}),
]
);
}
}

View file

@ -114,7 +114,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await; .await;
picker.update(cx, |picker, _| { picker.update(cx, |picker, _| {
assert_eq!( assert_eq!(
collect_search_results(picker), collect_search_matches(picker).search_only(),
vec![PathBuf::from("a/b/file2.txt")], vec![PathBuf::from("a/b/file2.txt")],
"Matching abs path should be the only match" "Matching abs path should be the only match"
) )
@ -136,7 +136,7 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
.await; .await;
picker.update(cx, |picker, _| { picker.update(cx, |picker, _| {
assert_eq!( assert_eq!(
collect_search_results(picker), collect_search_matches(picker).search_only(),
Vec::<PathBuf>::new(), Vec::<PathBuf>::new(),
"Mismatching abs path should produce no matches" "Mismatching abs path should produce no matches"
) )
@ -169,7 +169,7 @@ async fn test_complex_path(cx: &mut TestAppContext) {
picker.update(cx, |picker, _| { picker.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 1); assert_eq!(picker.delegate.matches.len(), 1);
assert_eq!( assert_eq!(
collect_search_results(picker), collect_search_matches(picker).search_only(),
vec![PathBuf::from("其他/S数据表格/task.xlsx")], vec![PathBuf::from("其他/S数据表格/task.xlsx")],
) )
}); });
@ -486,7 +486,7 @@ async fn test_single_file_worktrees(cx: &mut TestAppContext) {
assert_eq!(matches.len(), 1); assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) = let (file_name, file_name_positions, full_path, full_path_positions) =
delegate.labels_for_path_match(&matches[0]); delegate.labels_for_path_match(&matches[0].0);
assert_eq!(file_name, "the-file"); assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]); assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "the-file"); assert_eq!(full_path, "the-file");
@ -556,9 +556,9 @@ async fn test_path_distance_ordering(cx: &mut TestAppContext) {
delegate.matches.history.is_empty(), delegate.matches.history.is_empty(),
"Search matches expected" "Search matches expected"
); );
let matches = delegate.matches.search.clone(); let matches = &delegate.matches.search;
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); assert_eq!(matches[0].0.path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); assert_eq!(matches[1].0.path.as_ref(), Path::new("dir1/a.txt"));
}); });
} }
@ -957,7 +957,7 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
Some(PathBuf::from("/src/test/first.rs")) Some(PathBuf::from("/src/test/first.rs"))
)); ));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
}); });
let second_query = "fsdasdsa"; let second_query = "fsdasdsa";
@ -1002,10 +1002,65 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
Some(PathBuf::from("/src/test/first.rs")) Some(PathBuf::from("/src/test/first.rs"))
)); ));
assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); assert_eq!(delegate.matches.search.first().unwrap().0.path.as_ref(), Path::new("test/fourth.rs"));
}); });
} }
#[gpui::test]
async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"test": {
"1_qw": "// First file that matches the query",
"2_second": "// Second file",
"3_third": "// Third file",
"4_fourth": "// Fourth file",
"5_qwqwqw": "// A file with 3 more matches than the first one",
"6_qwqwqw": "// Same query matches as above, but closer to the end of the list due to the name",
"7_qwqwqw": "// One more, same amount of query matches as above",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
// generate some history to select from
open_close_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
open_close_queried_buffer("3", 1, "3_third", &workspace, cx).await;
open_close_queried_buffer("2", 1, "2_second", &workspace, cx).await;
open_close_queried_buffer("6", 1, "6_qwqwqw", &workspace, cx).await;
let finder = open_file_picker(&workspace, cx);
let query = "qw";
finder
.update(cx, |finder, cx| {
finder.delegate.update_matches(query.to_string(), cx)
})
.await;
finder.update(cx, |finder, _| {
let search_matches = collect_search_matches(finder);
assert_eq!(
search_matches.history,
vec![PathBuf::from("test/1_qw"), PathBuf::from("test/6_qwqwqw"),],
);
assert_eq!(
search_matches.search,
vec![
PathBuf::from("test/5_qwqwqw"),
PathBuf::from("test/7_qwqwqw"),
],
);
});
}
#[gpui::test] #[gpui::test]
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
@ -1048,14 +1103,14 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
.matches .matches
.search .search
.iter() .iter()
.map(|path_match| path_match.path.to_path_buf()) .map(|path_match| path_match.0.path.to_path_buf())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
search_entries, search_entries,
vec![ vec![
PathBuf::from("collab_ui/collab_ui.rs"), PathBuf::from("collab_ui/collab_ui.rs"),
PathBuf::from("collab_ui/third.rs"),
PathBuf::from("collab_ui/first.rs"), PathBuf::from("collab_ui/first.rs"),
PathBuf::from("collab_ui/third.rs"),
PathBuf::from("collab_ui/second.rs"), PathBuf::from("collab_ui/second.rs"),
], ],
"Despite all search results having the same directory name, the most matching one should be on top" "Despite all search results having the same directory name, the most matching one should be on top"
@ -1097,7 +1152,7 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext)
.matches .matches
.history .history
.iter() .iter()
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").0.path.to_path_buf())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
history_entries, history_entries,
@ -1124,7 +1179,8 @@ async fn open_close_queried_buffer(
assert_eq!( assert_eq!(
finder.delegate.matches.len(), finder.delegate.matches.len(),
expected_matches, expected_matches,
"Unexpected number of matches found for query {input}" "Unexpected number of matches found for query `{input}`, matches: {:?}",
finder.delegate.matches
); );
finder.delegate.history_items.clone() finder.delegate.history_items.clone()
}); });
@ -1137,7 +1193,7 @@ async fn open_close_queried_buffer(
let active_editor_title = active_editor.read(cx).title(cx); let active_editor_title = active_editor.read(cx).title(cx);
assert_eq!( assert_eq!(
expected_editor_title, active_editor_title, expected_editor_title, active_editor_title,
"Unexpected editor title for query {input}" "Unexpected editor title for query `{input}`"
); );
}); });
@ -1210,18 +1266,49 @@ fn active_file_picker(
}) })
} }
fn collect_search_results(picker: &Picker<FileFinderDelegate>) -> Vec<PathBuf> { #[derive(Debug)]
let matches = &picker.delegate.matches; struct SearchEntries {
assert!( history: Vec<PathBuf>,
matches.history.is_empty(), search: Vec<PathBuf>,
"Should have no history matches, but got: {:?}", }
matches.history
); impl SearchEntries {
let mut results = matches #[track_caller]
.search fn search_only(self) -> Vec<PathBuf> {
.iter() assert!(
.map(|path_match| Path::new(path_match.path_prefix.as_ref()).join(&path_match.path)) self.history.is_empty(),
.collect::<Vec<_>>(); "Should have no history matches, but got: {:?}",
results.sort(); self.history
results );
self.search
}
}
fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries {
let matches = &picker.delegate.matches;
SearchEntries {
history: matches
.history
.iter()
.map(|(history_path, path_match)| {
path_match
.as_ref()
.map(|path_match| {
Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path)
})
.unwrap_or_else(|| {
history_path
.absolute
.as_deref()
.unwrap_or_else(|| &history_path.project.path)
.to_path_buf()
})
})
.collect(),
search: matches
.search
.iter()
.map(|path_match| Path::new(path_match.0.path_prefix.as_ref()).join(&path_match.0.path))
.collect(),
}
} }

View file

@ -76,7 +76,7 @@ cbindgen = "0.26.0"
media = { path = "../media" } media = { path = "../media" }
anyhow.workspace = true anyhow.workspace = true
block = "0.1" block = "0.1"
cocoa = "0.24" cocoa = "0.25"
core-foundation = { version = "0.9.3", features = ["with-uuid"] } core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3" core-graphics = "0.22.3"
core-text = "19.2" core-text = "19.2"

View file

@ -0,0 +1,29 @@
use gpui::*;
struct HelloWorld {
text: SharedString,
}
impl Render for HelloWorld {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.size_full()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, {}!", &self.text))
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|_cx| HelloWorld {
text: "World".into(),
})
});
});
}

View file

@ -574,7 +574,7 @@ impl AppContext {
} }
/// Displays a platform modal for selecting a new path where a file can be saved. /// Displays a platform modal for selecting a new path where a file can be saved.
/// The provided directory will be used to set the iniital location. /// The provided directory will be used to set the initial location.
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel. /// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
/// If cancelled, a `None` will be relayed instead. /// If cancelled, a `None` will be relayed instead.
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> { pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {

View file

@ -40,7 +40,7 @@ impl Overlay {
self self
} }
/// Sets the position in window co-ordinates /// Sets the position in window coordinates
/// (otherwise the location the overlay is rendered is used) /// (otherwise the location the overlay is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self { pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor); self.anchor_position = Some(anchor);

View file

@ -150,7 +150,13 @@ pub(crate) trait PlatformWindow {
fn as_any_mut(&mut self) -> &mut dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler); fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>; fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>; fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize>;
fn activate(&self); fn activate(&self);
fn set_title(&mut self, title: &str); fn set_title(&mut self, title: &str);
fn set_edited(&mut self, edited: bool); fn set_edited(&mut self, edited: bool);

View file

@ -534,67 +534,77 @@ impl Platform for MacPlatform {
&self, &self,
options: PathPromptOptions, options: PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>> { ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
unsafe { let (done_tx, done_rx) = oneshot::channel();
let panel = NSOpenPanel::openPanel(nil); self.foreground_executor()
panel.setCanChooseDirectories_(options.directories.to_objc()); .spawn(async move {
panel.setCanChooseFiles_(options.files.to_objc()); unsafe {
panel.setAllowsMultipleSelection_(options.multiple.to_objc()); let panel = NSOpenPanel::openPanel(nil);
panel.setResolvesAliases_(false.to_objc()); panel.setCanChooseDirectories_(options.directories.to_objc());
let (done_tx, done_rx) = oneshot::channel(); panel.setCanChooseFiles_(options.files.to_objc());
let done_tx = Cell::new(Some(done_tx)); panel.setAllowsMultipleSelection_(options.multiple.to_objc());
let block = ConcreteBlock::new(move |response: NSModalResponse| { panel.setResolvesAliases_(false.to_objc());
let result = if response == NSModalResponse::NSModalResponseOk { let done_tx = Cell::new(Some(done_tx));
let mut result = Vec::new(); let block = ConcreteBlock::new(move |response: NSModalResponse| {
let urls = panel.URLs(); let result = if response == NSModalResponse::NSModalResponseOk {
for i in 0..urls.count() { let mut result = Vec::new();
let url = urls.objectAtIndex(i); let urls = panel.URLs();
if url.isFileURL() == YES { for i in 0..urls.count() {
if let Ok(path) = ns_url_to_path(url) { let url = urls.objectAtIndex(i);
result.push(path) if url.isFileURL() == YES {
if let Ok(path) = ns_url_to_path(url) {
result.push(path)
}
}
} }
} Some(result)
} } else {
Some(result) None
} else { };
None
};
if let Some(done_tx) = done_tx.take() { if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result); let _ = done_tx.send(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
} }
}); })
let block = block.copy(); .detach();
let _: () = msg_send![panel, beginWithCompletionHandler: block]; done_rx
done_rx
}
} }
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> { fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
unsafe { let directory = directory.to_owned();
let panel = NSSavePanel::savePanel(nil); let (done_tx, done_rx) = oneshot::channel();
let path = ns_string(directory.to_string_lossy().as_ref()); self.foreground_executor()
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc()); .spawn(async move {
panel.setDirectoryURL(url); unsafe {
let panel = NSSavePanel::savePanel(nil);
let path = ns_string(directory.to_string_lossy().as_ref());
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
panel.setDirectoryURL(url);
let (done_tx, done_rx) = oneshot::channel(); let done_tx = Cell::new(Some(done_tx));
let done_tx = Cell::new(Some(done_tx)); let block = ConcreteBlock::new(move |response: NSModalResponse| {
let block = ConcreteBlock::new(move |response: NSModalResponse| { let mut result = None;
let mut result = None; if response == NSModalResponse::NSModalResponseOk {
if response == NSModalResponse::NSModalResponseOk { let url = panel.URL();
let url = panel.URL(); if url.isFileURL() == YES {
if url.isFileURL() == YES { result = ns_url_to_path(panel.URL()).ok()
result = ns_url_to_path(panel.URL()).ok() }
} }
}
if let Some(done_tx) = done_tx.take() { if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(result); let _ = done_tx.send(result);
}
});
let block = block.copy();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
} }
}); })
let block = block.copy(); .detach();
let _: () = msg_send![panel, beginWithCompletionHandler: block];
done_rx done_rx
}
} }
fn reveal_path(&self, path: &Path) { fn reveal_path(&self, path: &Path) {

View file

@ -772,7 +772,13 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().input_handler.take() self.0.as_ref().lock().input_handler.take()
} }
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> { fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
// macOs applies overrides to modal window buttons after they are added. // macOs applies overrides to modal window buttons after they are added.
// Two most important for this logic are: // Two most important for this logic are:
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal // * Buttons with "Cancel" title will be displayed as the last buttons in the modal
@ -808,6 +814,9 @@ impl PlatformWindow for MacWindow {
}; };
let _: () = msg_send![alert, setAlertStyle: alert_style]; let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)]; let _: () = msg_send![alert, setMessageText: ns_string(msg)];
if let Some(detail) = detail {
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
}
for (ix, answer) in answers for (ix, answer) in answers
.iter() .iter()

View file

@ -185,6 +185,7 @@ impl PlatformWindow for TestWindow {
&self, &self,
_level: crate::PromptLevel, _level: crate::PromptLevel,
_msg: &str, _msg: &str,
_detail: Option<&str>,
_answers: &[&str], _answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> { ) -> futures::channel::oneshot::Receiver<usize> {
self.0 self.0

View file

@ -1478,9 +1478,12 @@ impl<'a> WindowContext<'a> {
&self, &self,
level: PromptLevel, level: PromptLevel,
message: &str, message: &str,
detail: Option<&str>,
answers: &[&str], answers: &[&str],
) -> oneshot::Receiver<usize> { ) -> oneshot::Receiver<usize> {
self.window.platform_window.prompt(level, message, answers) self.window
.platform_window
.prompt(level, message, detail, answers)
} }
/// Returns all available actions for the focused element. /// Returns all available actions for the focused element.

View file

@ -197,7 +197,7 @@ pub struct Diagnostic {
/// Whether this diagnostic is considered to originate from an analysis of /// Whether this diagnostic is considered to originate from an analysis of
/// files on disk, as opposed to any unsaved buffer contents. This is a /// files on disk, as opposed to any unsaved buffer contents. This is a
/// property of a given diagnostic source, and is configured for a given /// property of a given diagnostic source, and is configured for a given
/// language server via the [LspAdapter::disk_based_diagnostic_sources] method /// language server via the [`LspAdapter::disk_based_diagnostic_sources`](crate::LspAdapter::disk_based_diagnostic_sources) method
/// for the language server. /// for the language server.
pub is_disk_based: bool, pub is_disk_based: bool,
/// Whether this diagnostic marks unnecessary code. /// Whether this diagnostic marks unnecessary code.
@ -236,7 +236,7 @@ pub async fn prepare_completion_documentation(
} }
} }
/// Documentation associated with a [Completion]. /// Documentation associated with a [`Completion`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Documentation { pub enum Documentation {
/// There is no documentation for this completion. /// There is no documentation for this completion.
@ -301,7 +301,7 @@ pub enum Operation {
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
/// Whether the selections are in 'line mode'. /// Whether the selections are in 'line mode'.
line_mode: bool, line_mode: bool,
/// The [CursorShape] associated with these selections. /// The [`CursorShape`] associated with these selections.
cursor_shape: CursorShape, cursor_shape: CursorShape,
}, },
@ -347,7 +347,7 @@ pub enum Event {
/// The file associated with a buffer. /// The file associated with a buffer.
pub trait File: Send + Sync { pub trait File: Send + Sync {
/// Returns the [LocalFile] associated with this file, if the /// Returns the [`LocalFile`] associated with this file, if the
/// file is local. /// file is local.
fn as_local(&self) -> Option<&dyn LocalFile>; fn as_local(&self) -> Option<&dyn LocalFile>;
@ -378,7 +378,7 @@ pub trait File: Send + Sync {
/// Returns whether the file has been deleted. /// Returns whether the file has been deleted.
fn is_deleted(&self) -> bool; fn is_deleted(&self) -> bool;
/// Converts this file into an [Any] trait object. /// Converts this file into an [`Any`] trait object.
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
/// Converts this file into a protobuf message. /// Converts this file into a protobuf message.
@ -1538,8 +1538,6 @@ impl Buffer {
/// Starts a transaction, providing the current time. Subsequent transactions /// Starts a transaction, providing the current time. Subsequent transactions
/// that occur within a short period of time will be grouped together. This /// that occur within a short period of time will be grouped together. This
/// is controlled by the buffer's undo grouping duration. /// is controlled by the buffer's undo grouping duration.
///
/// See [`Buffer::set_group_interval`].
pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> { pub fn start_transaction_at(&mut self, now: Instant) -> Option<TransactionId> {
self.transaction_depth += 1; self.transaction_depth += 1;
if self.was_dirty_before_starting_transaction.is_none() { if self.was_dirty_before_starting_transaction.is_none() {
@ -1556,8 +1554,6 @@ impl Buffer {
/// Terminates the current transaction, providing the current time. Subsequent transactions /// Terminates the current transaction, providing the current time. Subsequent transactions
/// that occur within a short period of time will be grouped together. This /// that occur within a short period of time will be grouped together. This
/// is controlled by the buffer's undo grouping duration. /// is controlled by the buffer's undo grouping duration.
///
/// See [`Buffer::set_group_interval`].
pub fn end_transaction_at( pub fn end_transaction_at(
&mut self, &mut self,
now: Instant, now: Instant,
@ -2420,7 +2416,7 @@ impl BufferSnapshot {
} }
/// Iterates over chunks of text in the given range of the buffer. Text is chunked /// Iterates over chunks of text in the given range of the buffer. Text is chunked
/// in an arbitrary way due to being stored in a [`rope::Rope`]. The text is also /// in an arbitrary way due to being stored in a [`Rope`](text::Rope). The text is also
/// returned in chunks where each chunk has a single syntax highlighting style and /// returned in chunks where each chunk has a single syntax highlighting style and
/// diagnostic status. /// diagnostic status.
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks { pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks {

View file

@ -1657,7 +1657,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
let language = Language::new( let language = Language::new(
LanguageConfig { LanguageConfig {
name: "JavaScript".into(), name: "JavaScript".into(),
line_comment: Some("// ".into()), line_comments: vec!["// ".into()],
brackets: BracketPairConfig { brackets: BracketPairConfig {
pairs: vec![ pairs: vec![
BracketPair { BracketPair {
@ -1681,7 +1681,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
overrides: [( overrides: [(
"element".into(), "element".into(),
LanguageConfigOverride { LanguageConfigOverride {
line_comment: Override::Remove { remove: true }, line_comments: Override::Remove { remove: true },
block_comment: Override::Set(("{/*".into(), "*/}".into())), block_comment: Override::Set(("{/*".into(), "*/}".into())),
..Default::default() ..Default::default()
}, },
@ -1718,7 +1718,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
let config = snapshot.language_scope_at(0).unwrap(); let config = snapshot.language_scope_at(0).unwrap();
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// "); assert_eq!(config.line_comment_prefixes().unwrap(), &[Arc::from("// ")]);
// Both bracket pairs are enabled // Both bracket pairs are enabled
assert_eq!( assert_eq!(
config.brackets().map(|e| e.1).collect::<Vec<_>>(), config.brackets().map(|e| e.1).collect::<Vec<_>>(),
@ -1728,7 +1728,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
let string_config = snapshot let string_config = snapshot
.language_scope_at(text.find("b\"").unwrap()) .language_scope_at(text.find("b\"").unwrap())
.unwrap(); .unwrap();
assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// "); assert_eq!(
string_config.line_comment_prefixes().unwrap(),
&[Arc::from("// ")]
);
// Second bracket pair is disabled // Second bracket pair is disabled
assert_eq!( assert_eq!(
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(), string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
@ -1739,7 +1742,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
let element_config = snapshot let element_config = snapshot
.language_scope_at(text.find("<F>").unwrap()) .language_scope_at(text.find("<F>").unwrap())
.unwrap(); .unwrap();
assert_eq!(element_config.line_comment_prefix(), None); assert_eq!(element_config.line_comment_prefixes(), None);
assert_eq!( assert_eq!(
element_config.block_comment_delimiters(), element_config.block_comment_delimiters(),
Some((&"{/*".into(), &"*/}".into())) Some((&"{/*".into(), &"*/}".into()))
@ -1753,7 +1756,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
let tag_config = snapshot let tag_config = snapshot
.language_scope_at(text.find(" d=").unwrap() + 1) .language_scope_at(text.find(" d=").unwrap() + 1)
.unwrap(); .unwrap();
assert_eq!(tag_config.line_comment_prefix().unwrap().as_ref(), "// "); assert_eq!(
tag_config.line_comment_prefixes().unwrap(),
&[Arc::from("// ")]
);
assert_eq!( assert_eq!(
tag_config.brackets().map(|e| e.1).collect::<Vec<_>>(), tag_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true] &[true, true]
@ -1765,10 +1771,9 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
expression_in_element_config expression_in_element_config
.line_comment_prefix() .line_comment_prefixes()
.unwrap() .unwrap(),
.as_ref(), &[Arc::from("// ")]
"// "
); );
assert_eq!( assert_eq!(
expression_in_element_config expression_in_element_config
@ -1884,14 +1889,17 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap(); let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
assert_eq!(html_config.line_comment_prefix(), None); assert_eq!(html_config.line_comment_prefixes(), Some(&vec![]));
assert_eq!( assert_eq!(
html_config.block_comment_delimiters(), html_config.block_comment_delimiters(),
Some((&"<!--".into(), &"-->".into())) Some((&"<!--".into(), &"-->".into()))
); );
let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap(); let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# "); assert_eq!(
ruby_config.line_comment_prefixes().unwrap(),
&[Arc::from("# ")]
);
assert_eq!(ruby_config.block_comment_delimiters(), None); assert_eq!(ruby_config.block_comment_delimiters(), None);
buffer buffer
@ -2293,7 +2301,7 @@ fn ruby_lang() -> Language {
LanguageConfig { LanguageConfig {
name: "Ruby".into(), name: "Ruby".into(),
path_suffixes: vec!["rb".to_string()], path_suffixes: vec!["rb".to_string()],
line_comment: Some("# ".into()), line_comments: vec!["# ".into()],
..Default::default() ..Default::default()
}, },
Some(tree_sitter_ruby::language()), Some(tree_sitter_ruby::language()),

View file

@ -12,7 +12,7 @@ use text::{Anchor, FromAnchor, PointUtf16, ToOffset};
/// A set of diagnostics associated with a given buffer, provided /// A set of diagnostics associated with a given buffer, provided
/// by a single language server. /// by a single language server.
/// ///
/// The diagnostics are stored in a [SumTree], which allows this struct /// The diagnostics are stored in a [`SumTree`], which allows this struct
/// to be cheaply copied, and allows for efficient retrieval of the /// to be cheaply copied, and allows for efficient retrieval of the
/// diagnostics that intersect a given range of the buffer. /// diagnostics that intersect a given range of the buffer.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -21,9 +21,9 @@ pub struct DiagnosticSet {
} }
/// A single diagnostic in a set. Generic over its range type, because /// A single diagnostic in a set. Generic over its range type, because
/// the diagnostics are stored internally as [Anchor]s, but can be /// the diagnostics are stored internally as [`Anchor`]s, but can be
/// resolved to different coordinates types like [usize] byte offsets or /// resolved to different coordinates types like [`usize`] byte offsets or
/// [Point]s. /// [`Point`](gpui::Point)s.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiagnosticEntry<T> { pub struct DiagnosticEntry<T> {
/// The range of the buffer where the diagnostic applies. /// The range of the buffer where the diagnostic applies.
@ -52,7 +52,7 @@ pub struct Summary {
} }
impl<T> DiagnosticEntry<T> { impl<T> DiagnosticEntry<T> {
/// Returns a raw LSP diagnostic ssed to provide diagnostic context to lsp /// Returns a raw LSP diagnostic ssed to provide diagnostic context to LSP
/// codeAction request /// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
let code = self let code = self

View file

@ -139,12 +139,11 @@ pub struct CachedLspAdapter {
impl CachedLspAdapter { impl CachedLspAdapter {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> { pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await; let name = adapter.name();
let short_name = adapter.short_name(); let short_name = adapter.short_name();
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources();
let disk_based_diagnostics_progress_token = let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token();
adapter.disk_based_diagnostics_progress_token().await; let language_ids = adapter.language_ids();
let language_ids = adapter.language_ids().await;
Arc::new(CachedLspAdapter { Arc::new(CachedLspAdapter {
name, name,
@ -261,7 +260,7 @@ pub trait LspAdapterDelegate: Send + Sync {
#[async_trait] #[async_trait]
pub trait LspAdapter: 'static + Send + Sync { pub trait LspAdapter: 'static + Send + Sync {
async fn name(&self) -> LanguageServerName; fn name(&self) -> LanguageServerName;
fn short_name(&self) -> &'static str; fn short_name(&self) -> &'static str;
@ -299,10 +298,12 @@ pub trait LspAdapter: 'static + Send + Sync {
delegate: &dyn LspAdapterDelegate, delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary>; ) -> Option<LanguageServerBinary>;
/// Returns true if a language server can be reinstalled. /// Returns `true` if a language server can be reinstalled.
/// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is false. ///
/// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is `false`.
///
/// Implementations that rely on software already installed on user's system /// Implementations that rely on software already installed on user's system
/// should have [`can_be_reinstalled`] return false. /// should have [`can_be_reinstalled`](Self::can_be_reinstalled) return `false`.
fn can_be_reinstalled(&self) -> bool { fn can_be_reinstalled(&self) -> bool {
true true
} }
@ -314,7 +315,7 @@ pub trait LspAdapter: 'static + Send + Sync {
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
/// A callback called for each [`lsp_types::CompletionItem`] obtained from LSP server. /// A callback called for each [`lsp::CompletionItem`] obtained from LSP server.
/// Some LspAdapter implementations might want to modify the obtained item to /// Some LspAdapter implementations might want to modify the obtained item to
/// change how it's displayed. /// change how it's displayed.
async fn process_completion(&self, _: &mut lsp::CompletionItem) {} async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
@ -336,8 +337,8 @@ pub trait LspAdapter: 'static + Send + Sync {
None None
} }
/// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp_types::InitializeParams`] /// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp::InitializeParams`]
async fn initialization_options(&self) -> Option<Value> { fn initialization_options(&self) -> Option<Value> {
None None
} }
@ -356,15 +357,15 @@ pub trait LspAdapter: 'static + Send + Sync {
]) ])
} }
async fn disk_based_diagnostic_sources(&self) -> Vec<String> { fn disk_based_diagnostic_sources(&self) -> Vec<String> {
Default::default() Default::default()
} }
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> { fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
None None
} }
async fn language_ids(&self) -> HashMap<String, String> { fn language_ids(&self) -> HashMap<String, String> {
Default::default() Default::default()
} }
@ -417,8 +418,10 @@ pub struct LanguageConfig {
#[serde(default)] #[serde(default)]
pub collapsed_placeholder: String, pub collapsed_placeholder: String,
/// A line comment string that is inserted in e.g. `toggle comments` action. /// A line comment string that is inserted in e.g. `toggle comments` action.
/// A language can have multiple flavours of line comments. All of the provided line comments are
/// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments.
#[serde(default)] #[serde(default)]
pub line_comment: Option<Arc<str>>, pub line_comments: Vec<Arc<str>>,
/// Starting and closing characters of a block comment. /// Starting and closing characters of a block comment.
#[serde(default)] #[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>, pub block_comment: Option<(Arc<str>, Arc<str>)>,
@ -461,7 +464,7 @@ pub struct LanguageScope {
#[derive(Clone, Deserialize, Default, Debug)] #[derive(Clone, Deserialize, Default, Debug)]
pub struct LanguageConfigOverride { pub struct LanguageConfigOverride {
#[serde(default)] #[serde(default)]
pub line_comment: Override<Arc<str>>, pub line_comments: Override<Vec<Arc<str>>>,
#[serde(default)] #[serde(default)]
pub block_comment: Override<(Arc<str>, Arc<str>)>, pub block_comment: Override<(Arc<str>, Arc<str>)>,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
@ -507,7 +510,7 @@ impl Default for LanguageConfig {
increase_indent_pattern: Default::default(), increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(),
autoclose_before: Default::default(), autoclose_before: Default::default(),
line_comment: Default::default(), line_comments: Default::default(),
block_comment: Default::default(), block_comment: Default::default(),
scope_opt_in_language_servers: Default::default(), scope_opt_in_language_servers: Default::default(),
overrides: Default::default(), overrides: Default::default(),
@ -585,7 +588,7 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
} }
/// Describes a single bracket pair and how an editor should react to e.g. inserting /// Describes a single bracket pair and how an editor should react to e.g. inserting
/// an opening bracket or to a newline character insertion inbetween `start` and `end` characters. /// an opening bracket or to a newline character insertion in between `start` and `end` characters.
#[derive(Clone, Debug, Default, Deserialize, PartialEq)] #[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct BracketPair { pub struct BracketPair {
/// Starting substring for a bracket. /// Starting substring for a bracket.
@ -1711,10 +1714,10 @@ impl LanguageScope {
/// Returns line prefix that is inserted in e.g. line continuations or /// Returns line prefix that is inserted in e.g. line continuations or
/// in `toggle comments` action. /// in `toggle comments` action.
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> { pub fn line_comment_prefixes(&self) -> Option<&Vec<Arc<str>>> {
Override::as_option( Override::as_option(
self.config_override().map(|o| &o.line_comment), self.config_override().map(|o| &o.line_comments),
self.language.config.line_comment.as_ref(), Some(&self.language.config.line_comments),
) )
} }
@ -1881,7 +1884,7 @@ impl Default for FakeLspAdapter {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
#[async_trait] #[async_trait]
impl LspAdapter for Arc<FakeLspAdapter> { impl LspAdapter for Arc<FakeLspAdapter> {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName(self.name.into()) LanguageServerName(self.name.into())
} }
@ -1919,15 +1922,15 @@ impl LspAdapter for Arc<FakeLspAdapter> {
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> { fn disk_based_diagnostic_sources(&self) -> Vec<String> {
self.disk_based_diagnostics_sources.clone() self.disk_based_diagnostics_sources.clone()
} }
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> { fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
self.disk_based_diagnostics_progress_token.clone() self.disk_based_diagnostics_progress_token.clone()
} }
async fn initialization_options(&self) -> Option<Value> { fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone() self.initialization_options.clone()
} }

View file

@ -103,7 +103,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
} }
} }
/// Serializes an [`operation::EditOperation`] to be sent over RPC. /// Serializes an [`EditOperation`] to be sent over RPC.
pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit {
proto::operation::Edit { proto::operation::Edit {
replica_id: operation.timestamp.replica_id as u32, replica_id: operation.timestamp.replica_id as u32,

View file

@ -53,7 +53,7 @@ async-trait.workspace = true
block = "0.1" block = "0.1"
bytes = "1.2" bytes = "1.2"
byteorder = "1.4" byteorder = "1.4"
cocoa = "0.24" cocoa = "0.25"
core-foundation = "0.9.3" core-foundation = "0.9.3"
core-graphics = "0.22.3" core-graphics = "0.22.3"
foreign-types = "0.3" foreign-types = "0.3"

View file

@ -590,6 +590,7 @@ impl Project {
client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_expand_project_entry); client.add_model_request_handler(Self::handle_expand_project_entry);
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
client.add_model_request_handler(Self::handle_apply_code_action); 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_on_type_formatting);
client.add_model_request_handler(Self::handle_inlay_hints); client.add_model_request_handler(Self::handle_inlay_hints);
@ -974,8 +975,7 @@ impl Project {
// Start all the newly-enabled language servers. // Start all the newly-enabled language servers.
for (worktree, language) in language_servers_to_start { for (worktree, language) in language_servers_to_start {
let worktree_path = worktree.read(cx).abs_path(); self.start_language_servers(&worktree, language, cx);
self.start_language_servers(&worktree, worktree_path, language, cx);
} }
// Restart all language servers with changed initialization options. // Restart all language servers with changed initialization options.
@ -2774,8 +2774,8 @@ impl Project {
}; };
if let Some(file) = buffer_file { if let Some(file) = buffer_file {
let worktree = file.worktree.clone(); let worktree = file.worktree.clone();
if let Some(tree) = worktree.read(cx).as_local() { if worktree.read(cx).is_local() {
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx); self.start_language_servers(&worktree, new_language, cx);
} }
} }
} }
@ -2783,7 +2783,6 @@ impl Project {
fn start_language_servers( fn start_language_servers(
&mut self, &mut self,
worktree: &Model<Worktree>, worktree: &Model<Worktree>,
worktree_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
@ -2793,22 +2792,14 @@ impl Project {
return; return;
} }
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
self.start_language_server( self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
worktree_id,
worktree_path.clone(),
adapter.clone(),
language.clone(),
cx,
);
} }
} }
fn start_language_server( fn start_language_server(
&mut self, &mut self,
worktree_id: WorktreeId, worktree: &Model<Worktree>,
worktree_path: Arc<Path>,
adapter: Arc<CachedLspAdapter>, adapter: Arc<CachedLspAdapter>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
@ -2817,6 +2808,9 @@ impl Project {
return; return;
} }
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone()); let key = (worktree_id, adapter.name.clone());
if self.language_server_ids.contains_key(&key) { if self.language_server_ids.contains_key(&key) {
return; return;
@ -2949,20 +2943,14 @@ impl Project {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let worktrees = this.worktrees.clone(); let worktrees = this.worktrees.clone();
for worktree in worktrees { for worktree in worktrees {
let worktree = match worktree.upgrade() { if let Some(worktree) = worktree.upgrade() {
Some(worktree) => worktree.read(cx), this.start_language_server(
None => continue, &worktree,
}; adapter.clone(),
let worktree_id = worktree.id(); language.clone(),
let root_path = worktree.abs_path(); cx,
);
this.start_language_server( }
worktree_id,
root_path,
adapter.clone(),
language.clone(),
cx,
);
} }
}) })
.ok(); .ok();
@ -3176,7 +3164,7 @@ impl Project {
} }
}) })
.detach(); .detach();
let mut initialization_options = adapter.adapter.initialization_options().await; let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) { match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => { (Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options); merge_json_value_into(override_options, initialization_options);
@ -3332,7 +3320,7 @@ impl Project {
worktree_id: WorktreeId, worktree_id: WorktreeId,
adapter_name: LanguageServerName, adapter_name: LanguageServerName,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> { ) -> Task<Vec<WorktreeId>> {
let key = (worktree_id, adapter_name); let key = (worktree_id, adapter_name);
if let Some(server_id) = self.language_server_ids.remove(&key) { if let Some(server_id) = self.language_server_ids.remove(&key) {
log::info!("stopping language server {}", key.1 .0); log::info!("stopping language server {}", key.1 .0);
@ -3370,8 +3358,6 @@ impl Project {
let server_state = self.language_servers.remove(&server_id); let server_state = self.language_servers.remove(&server_id);
cx.emit(Event::LanguageServerRemoved(server_id)); cx.emit(Event::LanguageServerRemoved(server_id));
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let mut root_path = None;
let server = match server_state { let server = match server_state {
Some(LanguageServerState::Starting(task)) => task.await, Some(LanguageServerState::Starting(task)) => task.await,
Some(LanguageServerState::Running { server, .. }) => Some(server), Some(LanguageServerState::Running { server, .. }) => Some(server),
@ -3379,7 +3365,6 @@ impl Project {
}; };
if let Some(server) = server { if let Some(server) = server {
root_path = Some(server.root_path().clone());
if let Some(shutdown) = server.shutdown() { if let Some(shutdown) = server.shutdown() {
shutdown.await; shutdown.await;
} }
@ -3393,10 +3378,10 @@ impl Project {
.ok(); .ok();
} }
(root_path, orphaned_worktrees) orphaned_worktrees
}) })
} else { } else {
Task::ready((None, Vec::new())) Task::ready(Vec::new())
} }
} }
@ -3426,7 +3411,6 @@ impl Project {
None None
} }
// TODO This will break in the case where the adapter's root paths and worktrees are not equal
fn restart_language_servers( fn restart_language_servers(
&mut self, &mut self,
worktree: Model<Worktree>, worktree: Model<Worktree>,
@ -3434,50 +3418,42 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let worktree_id = worktree.read(cx).id(); let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new(); let stop_tasks = language
for adapter in language.lsp_adapters() { .lsp_adapters()
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); .iter()
} .map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
if stops.is_empty() { (stop_task, adapter.name.clone())
})
.collect::<Vec<_>>();
if stop_tasks.is_empty() {
return; return;
} }
let mut stops = stops.into_iter();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let (original_root_path, mut orphaned_worktrees) = stops.next().unwrap().await; // For each stopped language server, record all of the worktrees with which
for stop in stops { // it was associated.
let (_, worktrees) = stop.await; let mut affected_worktrees = Vec::new();
orphaned_worktrees.extend_from_slice(&worktrees); for (stop_task, language_server_name) in stop_tasks {
for affected_worktree_id in stop_task.await {
affected_worktrees.push((affected_worktree_id, language_server_name.clone()));
}
} }
let this = match this.upgrade() {
Some(this) => this,
None => return,
};
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
// Attempt to restart using original server path. Fallback to passed in // Restart the language server for the given worktree.
// path if we could not retrieve the root path this.start_language_servers(&worktree, language.clone(), cx);
let root_path = original_root_path
.map(|path_buf| Arc::from(path_buf.as_path()))
.unwrap_or(fallback_path);
this.start_language_servers(&worktree, root_path, language.clone(), cx);
// Lookup new server ids and set them for each of the orphaned worktrees // Lookup new server ids and set them for each of the orphaned worktrees
for adapter in language.lsp_adapters() { for (affected_worktree_id, language_server_name) in affected_worktrees {
if let Some(new_server_id) = this if let Some(new_server_id) = this
.language_server_ids .language_server_ids
.get(&(worktree_id, adapter.name.clone())) .get(&(worktree_id, language_server_name.clone()))
.cloned() .cloned()
{ {
for &orphaned_worktree in &orphaned_worktrees { this.language_server_ids
this.language_server_ids .insert((affected_worktree_id, language_server_name), new_server_id);
.insert((orphaned_worktree, adapter.name.clone()), new_server_id);
}
} }
} }
}) })
@ -7751,6 +7727,40 @@ impl Project {
}) })
} }
async fn handle_resolve_completion_documentation(
this: Model<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( async fn handle_apply_code_action(
this: Model<Self>, this: Model<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>, envelope: TypedEnvelope<proto::ApplyCodeAction>,

View file

@ -194,12 +194,14 @@ impl AsRef<Path> for RepositoryWorkDirectory {
pub struct WorkDirectoryEntry(ProjectEntryId); pub struct WorkDirectoryEntry(ProjectEntryId);
impl WorkDirectoryEntry { impl WorkDirectoryEntry {
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> { pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> {
worktree.entry_for_id(self.0).and_then(|entry| { let entry = worktree
path.strip_prefix(&entry.path) .entry_for_id(self.0)
.ok() .ok_or_else(|| anyhow!("entry not found"))?;
.map(move |path| path.into()) let path = path
}) .strip_prefix(&entry.path)
.map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?;
Ok(path.into())
} }
} }
@ -970,13 +972,15 @@ impl LocalWorktree {
let mut index_task = None; let mut index_task = None;
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
if let Some(repo) = snapshot.repository_for_path(&path) { if let Some(repo) = snapshot.repository_for_path(&path) {
let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap(); if let Some(repo_path) = repo.work_directory.relativize(&snapshot, &path).log_err()
if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) { {
let repo = repo.repo_ptr.clone(); if let Some(git_repo) = snapshot.git_repositories.get(&*repo.work_directory) {
index_task = Some( let git_repo = git_repo.repo_ptr.clone();
cx.background_executor() index_task = Some(
.spawn(async move { repo.lock().load_index_text(&repo_path) }), cx.background_executor()
); .spawn(async move { git_repo.lock().load_index_text(&repo_path) }),
);
}
} }
} }

View file

@ -778,6 +778,7 @@ impl ProjectPanel {
let answer = cx.prompt( let answer = cx.prompt(
PromptLevel::Info, PromptLevel::Info,
&format!("Delete {file_name:?}?"), &format!("Delete {file_name:?}?"),
None,
&["Delete", "Cancel"], &["Delete", "Cancel"],
); );

View file

@ -197,6 +197,19 @@ message Ack {}
message Error { message Error {
string message = 1; string message = 1;
ErrorCode code = 2;
repeated string tags = 3;
}
enum ErrorCode {
Internal = 0;
NoSuchChannel = 1;
Disconnected = 2;
SignedOut = 3;
UpgradeRequired = 4;
Forbidden = 5;
WrongReleaseChannel = 6;
NeedsCla = 7;
} }
message Test { message Test {

223
crates/rpc/src/error.rs Normal file
View file

@ -0,0 +1,223 @@
/// Some helpers for structured error handling.
///
/// The helpers defined here allow you to pass type-safe error codes from
/// the collab server to the client; and provide a mechanism for additional
/// structured data alongside the message.
///
/// When returning an error, it can be as simple as:
///
/// `return Err(Error::Forbidden.into())`
///
/// If you'd like to log more context, you can set a message. These messages
/// show up in our logs, but are not shown visibly to users.
///
/// `return Err(Error::Forbidden.message("not an admin").into())`
///
/// If you'd like to provide enough context that the UI can render a good error
/// message (or would be helpful to see in a structured format in the logs), you
/// can use .with_tag():
///
/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())`
///
/// When handling an error you can use .error_code() to match which error it was
/// and .error_tag() to read any tags.
///
/// ```
/// match err.error_code() {
/// ErrorCode::Forbidden => alert("I'm sorry I can't do that.")
/// ErrorCode::WrongReleaseChannel =>
/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap()))
/// ErrorCode::Internal => alert("Sorry, something went wrong")
/// }
/// ```
///
use crate::proto;
pub use proto::ErrorCode;
/// ErrorCodeExt provides some helpers for structured error handling.
///
/// The primary implementation is on the proto::ErrorCode to easily convert
/// that into an anyhow::Error, which we use pervasively.
///
/// The RpcError struct provides support for further metadata if needed.
pub trait ErrorCodeExt {
/// Return an anyhow::Error containing this.
/// (useful in places where .into() doesn't have enough type information)
fn anyhow(self) -> anyhow::Error;
/// Add a message to the error (by default the error code is used)
fn message(self, msg: String) -> RpcError;
/// Add a tag to the error. Tags are key value pairs that can be used
/// to send semi-structured data along with the error.
fn with_tag(self, k: &str, v: &str) -> RpcError;
}
impl ErrorCodeExt for proto::ErrorCode {
fn anyhow(self) -> anyhow::Error {
self.into()
}
fn message(self, msg: String) -> RpcError {
let err: RpcError = self.into();
err.message(msg)
}
fn with_tag(self, k: &str, v: &str) -> RpcError {
let err: RpcError = self.into();
err.with_tag(k, v)
}
}
/// ErrorExt provides helpers for structured error handling.
///
/// The primary implementation is on the anyhow::Error, which is
/// what we use throughout our codebase. Though under the hood this
pub trait ErrorExt {
/// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none)
fn error_code(&self) -> proto::ErrorCode;
/// error_tag() returns the value of the tag with the given key, if any.
fn error_tag(&self, k: &str) -> Option<&str>;
/// to_proto() converts the error into a proto::Error
fn to_proto(&self) -> proto::Error;
}
impl ErrorExt for anyhow::Error {
fn error_code(&self) -> proto::ErrorCode {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.code
} else {
proto::ErrorCode::Internal
}
}
fn error_tag(&self, k: &str) -> Option<&str> {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.error_tag(k)
} else {
None
}
}
fn to_proto(&self) -> proto::Error {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.to_proto()
} else {
ErrorCode::Internal.message(format!("{}", self)).to_proto()
}
}
}
impl From<proto::ErrorCode> for anyhow::Error {
fn from(value: proto::ErrorCode) -> Self {
RpcError {
request: None,
code: value,
msg: format!("{:?}", value).to_string(),
tags: Default::default(),
}
.into()
}
}
#[derive(Clone, Debug)]
pub struct RpcError {
request: Option<String>,
msg: String,
code: proto::ErrorCode,
tags: Vec<String>,
}
/// RpcError is a structured error type that is returned by the collab server.
/// In addition to a message, it lets you set a specific ErrorCode, and attach
/// small amounts of metadata to help the client handle the error appropriately.
///
/// This struct is not typically used directly, as we pass anyhow::Error around
/// in the app; however it is useful for chaining .message() and .with_tag() on
/// ErrorCode.
impl RpcError {
/// from_proto converts a proto::Error into an anyhow::Error containing
/// an RpcError.
pub fn from_proto(error: &proto::Error, request: &str) -> anyhow::Error {
RpcError {
request: Some(request.to_string()),
code: error.code(),
msg: error.message.clone(),
tags: error.tags.clone(),
}
.into()
}
}
impl ErrorCodeExt for RpcError {
fn message(mut self, msg: String) -> RpcError {
self.msg = msg;
self
}
fn with_tag(mut self, k: &str, v: &str) -> RpcError {
self.tags.push(format!("{}={}", k, v));
self
}
fn anyhow(self) -> anyhow::Error {
self.into()
}
}
impl ErrorExt for RpcError {
fn error_tag(&self, k: &str) -> Option<&str> {
for tag in &self.tags {
let mut parts = tag.split('=');
if let Some(key) = parts.next() {
if key == k {
return parts.next();
}
}
}
None
}
fn error_code(&self) -> proto::ErrorCode {
self.code
}
fn to_proto(&self) -> proto::Error {
proto::Error {
code: self.code as i32,
message: self.msg.clone(),
tags: self.tags.clone(),
}
}
}
impl std::error::Error for RpcError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl std::fmt::Display for RpcError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(request) = &self.request {
write!(f, "RPC request {} failed: {}", request, self.msg)?
} else {
write!(f, "{}", self.msg)?
}
for tag in &self.tags {
write!(f, " {}", tag)?
}
Ok(())
}
}
impl From<proto::ErrorCode> for RpcError {
fn from(code: proto::ErrorCode) -> Self {
RpcError {
request: None,
code,
msg: format!("{:?}", code).to_string(),
tags: Default::default(),
}
}
}

View file

@ -1,3 +1,5 @@
use crate::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError};
use super::{ use super::{
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage}, proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
Connection, Connection,
@ -423,11 +425,7 @@ impl Peer {
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?; let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
if let Some(proto::envelope::Payload::Error(error)) = &response.payload { if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
Err(anyhow!( Err(RpcError::from_proto(&error, T::NAME))
"RPC request {} failed - {}",
T::NAME,
error.message
))
} else { } else {
Ok(TypedEnvelope { Ok(TypedEnvelope {
message_id: response.id, message_id: response.id,
@ -516,9 +514,12 @@ impl Peer {
envelope: Box<dyn AnyTypedEnvelope>, envelope: Box<dyn AnyTypedEnvelope>,
) -> Result<()> { ) -> Result<()> {
let connection = self.connection_state(envelope.sender_id())?; let connection = self.connection_state(envelope.sender_id())?;
let response = proto::Error { let response = ErrorCode::Internal
message: format!("message {} was not handled", envelope.payload_type_name()), .message(format!(
}; "message {} was not handled",
envelope.payload_type_name()
))
.to_proto();
let message_id = connection let message_id = connection
.next_message_id .next_message_id
.fetch_add(1, atomic::Ordering::SeqCst); .fetch_add(1, atomic::Ordering::SeqCst);
@ -692,17 +693,17 @@ mod tests {
server server
.send( .send(
server_to_client_conn_id, server_to_client_conn_id,
proto::Error { ErrorCode::Internal
message: "message 1".to_string(), .message("message 1".to_string())
}, .to_proto(),
) )
.unwrap(); .unwrap();
server server
.send( .send(
server_to_client_conn_id, server_to_client_conn_id,
proto::Error { ErrorCode::Internal
message: "message 2".to_string(), .message("message 2".to_string())
}, .to_proto(),
) )
.unwrap(); .unwrap();
server.respond(request.receipt(), proto::Ack {}).unwrap(); server.respond(request.receipt(), proto::Ack {}).unwrap();
@ -797,17 +798,17 @@ mod tests {
server server
.send( .send(
server_to_client_conn_id, server_to_client_conn_id,
proto::Error { ErrorCode::Internal
message: "message 1".to_string(), .message("message 1".to_string())
}, .to_proto(),
) )
.unwrap(); .unwrap();
server server
.send( .send(
server_to_client_conn_id, server_to_client_conn_id,
proto::Error { ErrorCode::Internal
message: "message 2".to_string(), .message("message 2".to_string())
}, .to_proto(),
) )
.unwrap(); .unwrap();
server.respond(request1.receipt(), proto::Ack {}).unwrap(); server.respond(request1.receipt(), proto::Ack {}).unwrap();

View file

@ -1,10 +1,12 @@
pub mod auth; pub mod auth;
mod conn; mod conn;
mod error;
mod notification; mod notification;
mod peer; mod peer;
pub mod proto; pub mod proto;
pub use conn::Connection; pub use conn::Connection;
pub use error::*;
pub use notification::*; pub use notification::*;
pub use peer::*; pub use peer::*;
mod macros; mod macros;

View file

@ -1,3 +1,5 @@
mod registrar;
use crate::{ use crate::{
history::SearchHistory, history::SearchHistory,
mode::{next_mode, SearchMode}, mode::{next_mode, SearchMode},
@ -29,6 +31,9 @@ use workspace::{
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
}; };
pub use registrar::DivRegistrar;
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
#[derive(PartialEq, Clone, Deserialize)] #[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy { pub struct Deploy {
pub focus: bool, pub focus: bool,
@ -422,230 +427,59 @@ impl ToolbarItemView for BufferSearchBar {
} }
} }
/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
);
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
);
}
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut ViewContext<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
if search_bar.is_dismissed()
|| search_bar.active_searchable_item.is_none()
{
false
} else {
callback(search_bar, action, cx);
true
}
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
if search_bar.is_dismissed() {
callback(search_bar, action, cx);
true
} else {
false
}
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
}
/// Register actions for an active pane.
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
if search_bar.is_dismissed()
|| search_bar.active_searchable_item.is_none()
{
false
} else {
callback(search_bar, action, cx);
true
}
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
fn register_handler_for_dismissed_search<A: Action>(
&mut self,
callback: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
pane.update(cx, move |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
if search_bar.is_dismissed() {
callback(search_bar, action, cx);
true
} else {
false
}
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
}
impl BufferSearchBar { impl BufferSearchBar {
pub fn register(registrar: &mut impl SearchActionsRegistrar) { pub fn register(registrar: &mut impl SearchActionsRegistrar) {
registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| { registrar.register_handler(ForDeployed(|this, action: &ToggleCaseSensitive, cx| {
if this.supported_options().case { if this.supported_options().case {
this.toggle_case_sensitive(action, cx); this.toggle_case_sensitive(action, cx);
} }
}); }));
registrar.register_handler(|this, action: &ToggleWholeWord, cx| { registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, cx| {
if this.supported_options().word { if this.supported_options().word {
this.toggle_whole_word(action, cx); this.toggle_whole_word(action, cx);
} }
}); }));
registrar.register_handler(|this, action: &ToggleReplace, cx| { registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, cx| {
if this.supported_options().replacement { if this.supported_options().replacement {
this.toggle_replace(action, cx); this.toggle_replace(action, cx);
} }
}); }));
registrar.register_handler(|this, _: &ActivateRegexMode, cx| { registrar.register_handler(ForDeployed(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex { if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx); this.activate_search_mode(SearchMode::Regex, cx);
} }
}); }));
registrar.register_handler(|this, _: &ActivateTextMode, cx| { registrar.register_handler(ForDeployed(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx); this.activate_search_mode(SearchMode::Text, cx);
}); }));
registrar.register_handler(|this, action: &CycleMode, cx| { registrar.register_handler(ForDeployed(|this, action: &CycleMode, cx| {
if this.supported_options().regex { if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling. // cycling.
this.cycle_mode(action, cx) this.cycle_mode(action, cx)
} }
}); }));
registrar.register_handler(|this, action: &SelectNextMatch, cx| { registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
this.select_next_match(action, cx); this.select_next_match(action, cx);
}); }));
registrar.register_handler(|this, action: &SelectPrevMatch, cx| { registrar.register_handler(WithResults(|this, action: &SelectPrevMatch, cx| {
this.select_prev_match(action, cx); this.select_prev_match(action, cx);
}); }));
registrar.register_handler(|this, action: &SelectAllMatches, cx| { registrar.register_handler(WithResults(|this, action: &SelectAllMatches, cx| {
this.select_all_matches(action, cx); this.select_all_matches(action, cx);
}); }));
registrar.register_handler(|this, _: &editor::actions::Cancel, cx| { registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| {
this.dismiss(&Dismiss, cx); this.dismiss(&Dismiss, cx);
}); }));
// register deploy buffer search for both search bar states, since we want to focus into the search bar // register deploy buffer search for both search bar states, since we want to focus into the search bar
// when the deploy action is triggered in the buffer. // when the deploy action is triggered in the buffer.
registrar.register_handler(|this, deploy, cx| { registrar.register_handler(ForDeployed(|this, deploy, cx| {
this.deploy(deploy, cx); this.deploy(deploy, cx);
}); }));
registrar.register_handler_for_dismissed_search(|this, deploy, cx| { registrar.register_handler(ForDismissed(|this, deploy, cx| {
this.deploy(deploy, cx); this.deploy(deploy, cx);
}) }))
} }
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(cx: &mut ViewContext<Self>) -> Self {
@ -930,7 +764,7 @@ impl BufferSearchBar {
event: &editor::EditorEvent, event: &editor::EditorEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let editor::EditorEvent::Edited { .. } = event { if let editor::EditorEvent::Edited = event {
self.query_contains_error = false; self.query_contains_error = false;
self.clear_matches(cx); self.clear_matches(cx);
let search = self.update_matches(cx); let search = self.update_matches(cx);

View file

@ -0,0 +1,172 @@
use gpui::{div, Action, Div, InteractiveElement, View, ViewContext};
use workspace::Workspace;
use crate::BufferSearchBar;
/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are.
pub trait SearchActionsRegistrar {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>);
}
type SearchBarActionCallback<A> = fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>);
type GetSearchBar<T> =
for<'a, 'b> fn(&'a T, &'a mut ViewContext<'b, T>) -> Option<View<BufferSearchBar>>;
/// Registers search actions on a div that can be taken out.
pub struct DivRegistrar<'a, 'b, T: 'static> {
div: Option<Div>,
cx: &'a mut ViewContext<'b, T>,
search_getter: GetSearchBar<T>,
}
impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> {
pub fn new(search_getter: GetSearchBar<T>, cx: &'a mut ViewContext<'b, T>) -> Self {
Self {
div: Some(div()),
cx,
search_getter,
}
}
pub fn into_div(self) -> Div {
// This option is always Some; it's an option in the first place because we want to call methods
// on div that require ownership.
self.div.unwrap()
}
}
impl<T: 'static> SearchActionsRegistrar for DivRegistrar<'_, '_, T> {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
let getter = self.search_getter;
self.div = self.div.take().map(|div| {
div.on_action(self.cx.listener(move |this, action, cx| {
let should_notify = (getter)(this, cx)
.clone()
.map(|search_bar| {
search_bar.update(cx, |search_bar, cx| {
callback.execute(search_bar, action, cx)
})
})
.unwrap_or(false);
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}))
});
}
}
/// Register actions for an active pane.
impl SearchActionsRegistrar for Workspace {
fn register_handler<A: Action>(&mut self, callback: impl ActionExecutor<A>) {
self.register_action(move |workspace, action: &A, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let pane = workspace.active_pane();
let callback = callback.clone();
pane.update(cx, |this, cx| {
this.toolbar().update(cx, move |this, cx| {
if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
let should_notify = search_bar.update(cx, move |search_bar, cx| {
callback.execute(search_bar, action, cx)
});
if should_notify {
cx.notify();
} else {
cx.propagate();
}
}
})
});
});
}
}
type DidHandleAction = bool;
/// Potentially executes the underlying action if some preconditions are met (e.g. buffer search bar is visible)
pub trait ActionExecutor<A: Action>: 'static + Clone {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction;
}
/// Run an action when the search bar has been dismissed from the panel.
pub struct ForDismissed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDismissed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDismissed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() {
self.0(search_bar, action, cx);
true
} else {
false
}
}
}
/// Run an action when the search bar is deployed.
pub struct ForDeployed<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for ForDeployed<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for ForDeployed<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.is_dismissed() || search_bar.active_searchable_item.is_none() {
false
} else {
self.0(search_bar, action, cx);
true
}
}
}
/// Run an action when the search bar has any matches, regardless of whether it
/// is visible or not.
pub struct WithResults<A>(pub(super) SearchBarActionCallback<A>);
impl<A> Clone for WithResults<A> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<A: Action> ActionExecutor<A> for WithResults<A> {
fn execute(
&self,
search_bar: &mut BufferSearchBar,
action: &A,
cx: &mut ViewContext<BufferSearchBar>,
) -> DidHandleAction {
if search_bar.active_match_index.is_some() {
self.0(search_bar, action, cx);
true
} else {
false
}
}
}

View file

@ -88,6 +88,12 @@ pub fn init(cx: &mut AppContext) {
register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| { register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| {
search_bar.cycle_mode(action, cx) search_bar.cycle_mode(action, cx)
}); });
register_workspace_action(
workspace,
move |search_bar, action: &SelectPrevMatch, cx| {
search_bar.select_prev_match(action, cx)
},
);
register_workspace_action( register_workspace_action(
workspace, workspace,
move |search_bar, action: &SelectNextMatch, cx| { move |search_bar, action: &SelectNextMatch, cx| {
@ -746,6 +752,7 @@ impl ProjectSearchView {
cx.prompt( cx.prompt(
PromptLevel::Info, PromptLevel::Info,
prompt_text.as_str(), prompt_text.as_str(),
None,
&["Continue", "Cancel"], &["Continue", "Cancel"],
) )
})?; })?;
@ -1549,7 +1556,7 @@ impl ProjectSearchBar {
} }
} }
pub fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) { fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
if let Some(search) = self.active_project_search.as_ref() { if let Some(search) = self.active_project_search.as_ref() {
search.update(cx, |this, cx| { search.update(cx, |this, cx| {
this.select_match(Direction::Next, cx); this.select_match(Direction::Next, cx);

View file

@ -18,7 +18,7 @@ db = { path = "../db" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" } alacritty_terminal = "0.21"
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true

View file

@ -1,5 +1,4 @@
use alacritty_terminal::term::color::Rgb as AlacRgb; use alacritty_terminal::vte::ansi::Rgb as AlacRgb;
use gpui::Rgba; use gpui::Rgba;
//Convenience method to convert from a GPUI color to an alacritty Rgb //Convenience method to convert from a GPUI color to an alacritty Rgb
@ -8,5 +7,5 @@ pub fn to_alac_rgb(color: impl Into<Rgba>) -> AlacRgb {
let r = ((color.r * color.a) * 255.) as u8; let r = ((color.r * color.a) * 255.) as u8;
let g = ((color.g * color.a) * 255.) as u8; let g = ((color.g * color.a) * 255.) as u8;
let b = ((color.b * color.a) * 255.) as u8; let b = ((color.b * color.a) * 255.) as u8;
AlacRgb::new(r, g, b) AlacRgb { r, g, b }
} }

View file

@ -3,8 +3,6 @@ pub use alacritty_terminal;
pub mod terminal_settings; pub mod terminal_settings;
use alacritty_terminal::{ use alacritty_terminal::{
ansi::{ClearMode, Handler},
config::{Config, Program, PtyConfig, Scrolling},
event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
event_loop::{EventLoop, Msg, Notifier}, event_loop::{EventLoop, Msg, Notifier},
grid::{Dimensions, Scroll as AlacScroll}, grid::{Dimensions, Scroll as AlacScroll},
@ -13,11 +11,11 @@ use alacritty_terminal::{
sync::FairMutex, sync::FairMutex,
term::{ term::{
cell::Cell, cell::Cell,
color::Rgb,
search::{Match, RegexIter, RegexSearch}, search::{Match, RegexIter, RegexSearch},
RenderableCursor, TermMode, Config, RenderableCursor, TermMode,
}, },
tty::{self, setup_env}, tty::{self, setup_env},
vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode, Rgb},
Term, Term,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@ -58,7 +56,6 @@ use gpui::{
}; };
use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
use lazy_static::lazy_static;
actions!( actions!(
terminal, terminal,
@ -75,15 +72,6 @@ const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
const DEBUG_CELL_WIDTH: Pixels = px(5.); const DEBUG_CELL_WIDTH: Pixels = px(5.);
const DEBUG_LINE_HEIGHT: Pixels = px(5.); const DEBUG_LINE_HEIGHT: Pixels = px(5.);
lazy_static! {
// Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
// * avoid Rust-specific escaping.
// * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
}
///Upward flowing events, for changing the title and such ///Upward flowing events, for changing the title and such
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Event { pub enum Event {
@ -289,66 +277,70 @@ impl TerminalBuilder {
pub fn new( pub fn new(
working_directory: Option<PathBuf>, working_directory: Option<PathBuf>,
shell: Shell, shell: Shell,
mut env: HashMap<String, String>, env: HashMap<String, String>,
blink_settings: Option<TerminalBlink>, blink_settings: Option<TerminalBlink>,
alternate_scroll: AlternateScroll, alternate_scroll: AlternateScroll,
window: AnyWindowHandle, window: AnyWindowHandle,
) -> Result<TerminalBuilder> { ) -> Result<TerminalBuilder> {
let pty_config = { let pty_options = {
let alac_shell = match shell.clone() { let alac_shell = match shell.clone() {
Shell::System => None, Shell::System => None,
Shell::Program(program) => Some(Program::Just(program)), Shell::Program(program) => {
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }), Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))
}
Shell::WithArguments { program, args } => {
Some(alacritty_terminal::tty::Shell::new(program, args))
}
}; };
PtyConfig { alacritty_terminal::tty::Options {
shell: alac_shell, shell: alac_shell,
working_directory: working_directory.clone(), working_directory: working_directory.clone(),
hold: false, hold: false,
} }
}; };
//TODO: Properly set the current locale, // First, setup Alacritty's env
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string()); setup_env();
env.insert("ZED_TERM".to_string(), true.to_string());
let alac_scrolling = Scrolling::default(); // Then setup configured environment variables
// alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32); for (key, value) in env {
std::env::set_var(key, value);
}
//TODO: Properly set the current locale,
std::env::set_var("LC_ALL", "en_US.UTF-8");
std::env::set_var("ZED_TERM", "true");
let config = Config { let config = Config {
pty_config: pty_config.clone(), scrolling_history: 10000,
env,
scrolling: alac_scrolling,
..Default::default() ..Default::default()
}; };
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
//TODO: Remove with a bounded sender which can be dispatched on &self //TODO: Remove with a bounded sender which can be dispatched on &self
let (events_tx, events_rx) = unbounded(); let (events_tx, events_rx) = unbounded();
//Set up the terminal... //Set up the terminal...
let mut term = Term::new( let mut term = Term::new(
&config, config,
&TerminalSize::default(), &TerminalSize::default(),
ZedListener(events_tx.clone()), ZedListener(events_tx.clone()),
); );
//Start off blinking if we need to //Start off blinking if we need to
if let Some(TerminalBlink::On) = blink_settings { if let Some(TerminalBlink::On) = blink_settings {
term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor) term.set_private_mode(PrivateMode::Named(NamedPrivateMode::BlinkingCursor));
} }
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off. //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
if let AlternateScroll::Off = alternate_scroll { if let AlternateScroll::Off = alternate_scroll {
term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll) term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
} }
let term = Arc::new(FairMutex::new(term)); let term = Arc::new(FairMutex::new(term));
//Setup the pty... //Setup the pty...
let pty = match tty::new( let pty = match tty::new(
&pty_config, &pty_options,
TerminalSize::default().into(), TerminalSize::default().into(),
window.window_id().as_u64(), window.window_id().as_u64(),
) { ) {
@ -370,13 +362,16 @@ impl TerminalBuilder {
term.clone(), term.clone(),
ZedListener(events_tx.clone()), ZedListener(events_tx.clone()),
pty, pty,
pty_config.hold, pty_options.hold,
false, false,
); );
//Kick things off //Kick things off
let pty_tx = event_loop.channel(); let pty_tx = event_loop.channel();
let _io_thread = event_loop.spawn(); let _io_thread = event_loop.spawn(); // DANGER
let url_regex = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
let word_regex = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
let terminal = Terminal { let terminal = Terminal {
pty_tx: Notifier(pty_tx), pty_tx: Notifier(pty_tx),
@ -396,6 +391,8 @@ impl TerminalBuilder {
selection_phase: SelectionPhase::Ended, selection_phase: SelectionPhase::Ended,
cmd_pressed: false, cmd_pressed: false,
hovered_word: false, hovered_word: false,
url_regex,
word_regex,
}; };
Ok(TerminalBuilder { Ok(TerminalBuilder {
@ -514,7 +511,7 @@ impl Default for TerminalContent {
selection_text: Default::default(), selection_text: Default::default(),
selection: Default::default(), selection: Default::default(),
cursor: RenderableCursor { cursor: RenderableCursor {
shape: alacritty_terminal::ansi::CursorShape::Block, shape: alacritty_terminal::vte::ansi::CursorShape::Block,
point: AlacPoint::new(Line(0), Column(0)), point: AlacPoint::new(Line(0), Column(0)),
}, },
cursor_char: Default::default(), cursor_char: Default::default(),
@ -550,6 +547,8 @@ pub struct Terminal {
selection_phase: SelectionPhase, selection_phase: SelectionPhase,
cmd_pressed: bool, cmd_pressed: bool,
hovered_word: bool, hovered_word: bool,
url_regex: RegexSearch,
word_regex: RegexSearch,
} }
impl Terminal { impl Terminal {
@ -760,7 +759,7 @@ impl Terminal {
let url_match = min_index..=max_index; let url_match = min_index..=max_index;
Some((url, true, url_match)) Some((url, true, url_match))
} else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { } else if let Some(word_match) = regex_match_at(term, point, &mut self.word_regex) {
let maybe_url_or_path = let maybe_url_or_path =
term.bounds_to_string(*word_match.start(), *word_match.end()); term.bounds_to_string(*word_match.start(), *word_match.end());
let original_match = word_match.clone(); let original_match = word_match.clone();
@ -777,7 +776,7 @@ impl Terminal {
(word_match, maybe_url_or_path) (word_match, maybe_url_or_path)
}; };
let is_url = match regex_match_at(term, point, &URL_REGEX) { let is_url = match regex_match_at(term, point, &mut self.url_regex) {
Some(url_match) => { Some(url_match) => {
// `]` is a valid symbol in the `file://` URL, so the regex match will include it // `]` is a valid symbol in the `file://` URL, so the regex match will include it
// consider that when ensuring that the URL match is the same as the original word // consider that when ensuring that the URL match is the same as the original word
@ -1275,14 +1274,14 @@ impl Terminal {
pub fn find_matches( pub fn find_matches(
&mut self, &mut self,
searcher: RegexSearch, mut searcher: RegexSearch,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Vec<RangeInclusive<AlacPoint>>> { ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
let term = self.term.clone(); let term = self.term.clone();
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
let term = term.lock(); let term = term.lock();
all_search_matches(&term, &searcher).collect() all_search_matches(&term, &mut searcher).collect()
}) })
} }
@ -1332,7 +1331,7 @@ impl EventEmitter<Event> for Terminal {}
/// Based on alacritty/src/display/hint.rs > regex_match_at /// Based on alacritty/src/display/hint.rs > regex_match_at
/// Retrieve the match, if the specified point is inside the content matching the regex. /// Retrieve the match, if the specified point is inside the content matching the regex.
fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> Option<Match> { fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &mut RegexSearch) -> Option<Match> {
visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point)) visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
} }
@ -1340,7 +1339,7 @@ fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> O
/// Iterate over all visible regex matches. /// Iterate over all visible regex matches.
pub fn visible_regex_match_iter<'a, T>( pub fn visible_regex_match_iter<'a, T>(
term: &'a Term<T>, term: &'a Term<T>,
regex: &'a RegexSearch, regex: &'a mut RegexSearch,
) -> impl Iterator<Item = Match> + 'a { ) -> impl Iterator<Item = Match> + 'a {
let viewport_start = Line(-(term.grid().display_offset() as i32)); let viewport_start = Line(-(term.grid().display_offset() as i32));
let viewport_end = viewport_start + term.bottommost_line(); let viewport_end = viewport_start + term.bottommost_line();
@ -1362,7 +1361,7 @@ fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
fn all_search_matches<'a, T>( fn all_search_matches<'a, T>(
term: &'a Term<T>, term: &'a Term<T>,
regex: &'a RegexSearch, regex: &'a mut RegexSearch,
) -> impl Iterator<Item = Match> + 'a { ) -> impl Iterator<Item = Match> + 'a {
let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());

View file

@ -11,12 +11,11 @@ use itertools::Itertools;
use language::CursorShape; use language::CursorShape;
use settings::Settings; use settings::Settings;
use terminal::{ use terminal::{
alacritty_terminal::ansi::NamedColor,
alacritty_terminal::{ alacritty_terminal::{
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape},
grid::Dimensions, grid::Dimensions,
index::Point as AlacPoint, index::Point as AlacPoint,
term::{cell::Flags, TermMode}, term::{cell::Flags, TermMode},
vte::ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
}, },
terminal_settings::TerminalSettings, terminal_settings::TerminalSettings,
IndexedCell, Terminal, TerminalContent, TerminalSize, IndexedCell, Terminal, TerminalContent, TerminalSize,
@ -308,7 +307,7 @@ impl TerminalElement {
/// Converts the Alacritty cell styles to GPUI text styles and background color. /// Converts the Alacritty cell styles to GPUI text styles and background color.
fn cell_style( fn cell_style(
indexed: &IndexedCell, indexed: &IndexedCell,
fg: terminal::alacritty_terminal::ansi::Color, fg: terminal::alacritty_terminal::vte::ansi::Color,
// bg: terminal::alacritty_terminal::ansi::Color, // bg: terminal::alacritty_terminal::ansi::Color,
colors: &Theme, colors: &Theme,
text_style: &TextStyle, text_style: &TextStyle,
@ -998,11 +997,11 @@ fn to_highlighted_range_lines(
} }
/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent. /// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla { fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
let colors = theme.colors(); let colors = theme.colors();
match fg { match fg {
// Named and theme defined colors // Named and theme defined colors
terminal::alacritty_terminal::ansi::Color::Named(n) => match n { terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
NamedColor::Black => colors.terminal_ansi_black, NamedColor::Black => colors.terminal_ansi_black,
NamedColor::Red => colors.terminal_ansi_red, NamedColor::Red => colors.terminal_ansi_red,
NamedColor::Green => colors.terminal_ansi_green, NamedColor::Green => colors.terminal_ansi_green,
@ -1034,11 +1033,11 @@ fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme)
NamedColor::DimForeground => colors.terminal_dim_foreground, NamedColor::DimForeground => colors.terminal_dim_foreground,
}, },
// 'True' colors // 'True' colors
terminal::alacritty_terminal::ansi::Color::Spec(rgb) => { terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
terminal::rgba_color(rgb.r, rgb.g, rgb.b) terminal::rgba_color(rgb.r, rgb.g, rgb.b)
} }
// 8 bit, indexed colors // 8 bit, indexed colors
terminal::alacritty_terminal::ansi::Color::Indexed(i) => { terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
terminal::get_color_at_index(*i as usize, theme) terminal::get_color_at_index(*i as usize, theme)
} }
} }

View file

@ -911,7 +911,7 @@ impl SearchableItem for TerminalView {
} }
} }
///Get's the working directory for the given workspace, respecting the user's settings. ///Gets the working directory for the given workspace, respecting the user's settings.
pub fn get_working_directory( pub fn get_working_directory(
workspace: &Workspace, workspace: &Workspace,
cx: &AppContext, cx: &AppContext,
@ -932,7 +932,7 @@ pub fn get_working_directory(
res.or_else(home_dir) res.or_else(home_dir)
} }
///Get's the first project's home directory, or the home directory ///Gets the first project's home directory, or the home directory
fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> { fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
workspace workspace
.worktrees(cx) .worktrees(cx)

View file

@ -2634,7 +2634,7 @@ impl Default for LineEnding {
return Self::Unix; return Self::Unix;
#[cfg(not(unix))] #[cfg(not(unix))]
return Self::CRLF; return Self::Windows;
} }
} }

View file

@ -236,7 +236,7 @@ pub struct ThemeColors {
#[derive(Refineable, Clone)] #[derive(Refineable, Clone)]
pub struct ThemeStyles { pub struct ThemeStyles {
pub system: SystemColors, pub system: SystemColors,
/// An array of colors used for theme elements that iterrate through a series of colors. /// An array of colors used for theme elements that iterate through a series of colors.
/// ///
/// Example: Player colors, rainbow brackets and indent guides, etc. /// Example: Player colors, rainbow brackets and indent guides, etc.
pub accents: Vec<Hsla>, pub accents: Vec<Hsla>,

View file

@ -66,7 +66,7 @@ Copy that json file into the theme family directory and tidy up the filenames as
A LICENSE file is required to import a theme family. Failing to provide a complete text license will cause it to be skipped when the import is run. A LICENSE file is required to import a theme family. Failing to provide a complete text license will cause it to be skipped when the import is run.
If the theme only provices a license code (e.g. MIT, Apache 2.0, etc.) then put that code into the LICENSE file. If the theme only provides a license code (e.g. MIT, Apache 2.0, etc.) then put that code into the LICENSE file.
If no license is provided, either contact the theme creator or don't add the theme. If no license is provided, either contact the theme creator or don't add the theme.

View file

@ -27,7 +27,7 @@ pub enum AvatarShape {
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Avatar { pub struct Avatar {
image: Img, image: Img,
size: Option<Pixels>, size: Option<AbsoluteLength>,
border_color: Option<Hsla>, border_color: Option<Hsla>,
indicator: Option<AnyElement>, indicator: Option<AnyElement>,
} }
@ -82,8 +82,8 @@ impl Avatar {
} }
/// Size overrides the avatar size. By default they are 1rem. /// Size overrides the avatar size. By default they are 1rem.
pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self { pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
self.size = size.into(); self.size = size.into().map(Into::into);
self self
} }
@ -105,8 +105,8 @@ impl RenderOnce for Avatar {
px(0.) px(0.)
}; };
let image_size = self.size.unwrap_or_else(|| cx.rem_size()); let image_size = self.size.unwrap_or_else(|| rems(1.).into());
let container_size = image_size + border_width * 2.; let container_size = image_size.to_pixels(cx.rem_size()) + border_width * 2.;
div() div()
.size(container_size) .size(container_size)

View file

@ -59,7 +59,7 @@ pub async fn latest_github_release(
Err(_) => { Err(_) => {
log::error!( log::error!(
"Error deserializing Github API response text: {:?}", "Error deserializing GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice()) String::from_utf8_lossy(body.as_slice())
); );
return Err(anyhow!("error deserializing latest release")); return Err(anyhow!("error deserializing latest release"));

View file

@ -5,7 +5,7 @@ use serde_derive::Deserialize;
use workspace::{SaveIntent, Workspace}; use workspace::{SaveIntent, Workspace};
use crate::{ use crate::{
motion::{EndOfDocument, Motion}, motion::{EndOfDocument, Motion, StartOfDocument},
normal::{ normal::{
move_cursor, move_cursor,
search::{FindCommand, ReplaceCommand}, search::{FindCommand, ReplaceCommand},
@ -235,6 +235,8 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option<CommandInt
// goto (other ranges handled under _ => ) // goto (other ranges handled under _ => )
"$" => ("$", EndOfDocument.boxed_clone()), "$" => ("$", EndOfDocument.boxed_clone()),
"%" => ("%", EndOfDocument.boxed_clone()),
"0" => ("0", StartOfDocument.boxed_clone()),
_ => { _ => {
if query.starts_with("/") || query.starts_with("?") { if query.starts_with("/") || query.starts_with("?") {
@ -280,7 +282,7 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
return positions; return positions;
}; };
for (i, c) in string.chars().enumerate() { for (i, c) in string.char_indices() {
if c == current { if c == current {
positions.push(i); positions.push(i);
if let Some(c) = chars.next() { if let Some(c) = chars.next() {

View file

@ -1,8 +1,8 @@
use crate::{Toast, Workspace}; use crate::{Toast, Workspace};
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
Task, View, ViewContext, VisualContext, WindowContext, PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
}; };
use std::{any::TypeId, ops::DerefMut}; use std::{any::TypeId, ops::DerefMut};
@ -299,7 +299,7 @@ pub trait NotifyTaskExt {
impl<R, E> NotifyTaskExt for Task<Result<R, E>> impl<R, E> NotifyTaskExt for Task<Result<R, E>>
where where
E: std::fmt::Debug + 'static, E: std::fmt::Debug + Sized + 'static,
R: 'static, R: 'static,
{ {
fn detach_and_notify_err(self, cx: &mut WindowContext) { fn detach_and_notify_err(self, cx: &mut WindowContext) {
@ -307,3 +307,39 @@ where
.detach(); .detach();
} }
} }
pub trait DetachAndPromptErr {
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
);
}
impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
where
R: 'static,
{
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) {
let msg = msg.to_owned();
cx.spawn(|mut cx| async move {
if let Err(err) = self.await {
log::error!("{err:?}");
if let Ok(prompt) = cx.update(|cx| {
let detail = f(&err, cx)
.unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
}) {
prompt.await.ok();
}
}
})
.detach();
}
}

View file

@ -870,7 +870,7 @@ impl Pane {
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>, items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
all_dirty_items: usize, all_dirty_items: usize,
cx: &AppContext, cx: &AppContext,
) -> String { ) -> (String, String) {
/// Quantity of item paths displayed in prompt prior to cutoff.. /// Quantity of item paths displayed in prompt prior to cutoff..
const FILE_NAMES_CUTOFF_POINT: usize = 10; const FILE_NAMES_CUTOFF_POINT: usize = 10;
let mut file_names: Vec<_> = items let mut file_names: Vec<_> = items
@ -894,10 +894,12 @@ impl Pane {
file_names.push(format!(".. {} files not shown", not_shown_files).into()); file_names.push(format!(".. {} files not shown", not_shown_files).into());
} }
} }
let file_names = file_names.join("\n"); (
format!( format!(
"Do you want to save changes to the following {} files?\n{file_names}", "Do you want to save changes to the following {} files?",
all_dirty_items all_dirty_items
),
file_names.join("\n"),
) )
} }
@ -929,11 +931,12 @@ impl Pane {
cx.spawn(|pane, mut cx| async move { cx.spawn(|pane, mut cx| async move {
if save_intent == SaveIntent::Close && dirty_items.len() > 1 { if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = pane.update(&mut cx, |_, cx| { let answer = pane.update(&mut cx, |_, cx| {
let prompt = let (prompt, detail) =
Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx); Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
cx.prompt( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
&prompt, &prompt,
Some(&detail),
&["Save all", "Discard all", "Cancel"], &["Save all", "Discard all", "Cancel"],
) )
})?; })?;
@ -1131,6 +1134,7 @@ impl Pane {
cx.prompt( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
CONFLICT_MESSAGE, CONFLICT_MESSAGE,
None,
&["Overwrite", "Discard", "Cancel"], &["Overwrite", "Discard", "Cancel"],
) )
})?; })?;
@ -1154,6 +1158,7 @@ impl Pane {
cx.prompt( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
&prompt, &prompt,
None,
&["Save", "Don't Save", "Cancel"], &["Save", "Don't Save", "Cancel"],
) )
})?; })?;

View file

@ -14,8 +14,8 @@ mod workspace_settings;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use call::ActiveCall; use call::ActiveCall;
use client::{ use client::{
proto::{self, PeerId}, proto::{self, ErrorCode, PeerId},
Client, Status, TypedEnvelope, UserStore, Client, ErrorExt, Status, TypedEnvelope, UserStore,
}; };
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@ -30,8 +30,8 @@ use gpui::{
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle, DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
WindowBounds, WindowContext, WindowHandle, WindowOptions, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
}; };
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools; use itertools::Itertools;
@ -1159,6 +1159,7 @@ impl Workspace {
cx.prompt( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
"Do you want to leave the current call?", "Do you want to leave the current call?",
None,
&["Close window and hang up", "Cancel"], &["Close window and hang up", "Cancel"],
) )
})?; })?;
@ -1214,7 +1215,7 @@ impl Workspace {
// Override save mode and display "Save all files" prompt // Override save mode and display "Save all files" prompt
if save_intent == SaveIntent::Close && dirty_items.len() > 1 { if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = workspace.update(&mut cx, |_, cx| { let answer = workspace.update(&mut cx, |_, cx| {
let prompt = Pane::file_names_for_prompt( let (prompt, detail) = Pane::file_names_for_prompt(
&mut dirty_items.iter().map(|(_, handle)| handle), &mut dirty_items.iter().map(|(_, handle)| handle),
dirty_items.len(), dirty_items.len(),
cx, cx,
@ -1222,6 +1223,7 @@ impl Workspace {
cx.prompt( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
&prompt, &prompt,
Some(&detail),
&["Save all", "Discard all", "Cancel"], &["Save all", "Discard all", "Cancel"],
) )
})?; })?;
@ -3887,13 +3889,16 @@ async fn join_channel_internal(
if should_prompt { if should_prompt {
if let Some(workspace) = requesting_window { if let Some(workspace) = requesting_window {
let answer = workspace.update(cx, |_, cx| { let answer = workspace
cx.prompt( .update(cx, |_, cx| {
PromptLevel::Warning, cx.prompt(
"Leaving this call will unshare your current project.\nDo you want to switch channels?", PromptLevel::Warning,
&["Yes, Join Channel", "Cancel"], "Do you want to switch channels?",
) Some("Leaving this call will unshare your current project."),
})?.await; &["Yes, Join Channel", "Cancel"],
)
})?
.await;
if answer == Ok(1) { if answer == Ok(1) {
return Ok(false); return Ok(false);
@ -3919,10 +3924,10 @@ async fn join_channel_internal(
| Status::Reconnecting | Status::Reconnecting
| Status::Reauthenticating => continue, | Status::Reauthenticating => continue,
Status::Connected { .. } => break 'outer, Status::Connected { .. } => break 'outer,
Status::SignedOut => return Err(anyhow!("not signed in")), Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
Status::UpgradeRequired => return Err(anyhow!("zed is out of date")), Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
return Err(anyhow!("zed is offline")) return Err(ErrorCode::Disconnected.into())
} }
} }
} }
@ -3995,9 +4000,27 @@ pub fn join_channel(
if let Some(active_window) = active_window { if let Some(active_window) = active_window {
active_window active_window
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
let detail: SharedString = match err.error_code() {
ErrorCode::SignedOut => {
"Please sign in to continue.".into()
},
ErrorCode::UpgradeRequired => {
"Your are running an unsupported version of Zed. Please update to continue.".into()
},
ErrorCode::NoSuchChannel => {
"No matching channel was found. Please check the link and try again.".into()
},
ErrorCode::Forbidden => {
"This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
},
ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
ErrorCode::WrongReleaseChannel => format!("Others in the channel are using the {} release of Zed. Please switch to join this call.", err.error_tag("required").unwrap_or("other")).into(),
_ => format!("{}\n\nPlease try again.", err).into(),
};
cx.prompt( cx.prompt(
PromptLevel::Critical, PromptLevel::Critical,
&format!("Failed to join channel: {}", err), "Failed to join channel",
Some(&detail),
&["Ok"], &["Ok"],
) )
})? })?
@ -4224,6 +4247,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
cx.prompt( cx.prompt(
PromptLevel::Info, PromptLevel::Info,
"Are you sure you want to restart?", "Are you sure you want to restart?",
None,
&["Restart", "Cancel"], &["Restart", "Cancel"],
) )
}) })

View file

@ -121,6 +121,7 @@ tree-sitter-elixir.workspace = true
tree-sitter-elm.workspace = true tree-sitter-elm.workspace = true
tree-sitter-embedded-template.workspace = true tree-sitter-embedded-template.workspace = true
tree-sitter-glsl.workspace = true tree-sitter-glsl.workspace = true
tree-sitter-gleam.workspace = true
tree-sitter-go.workspace = true tree-sitter-go.workspace = true
tree-sitter-heex.workspace = true tree-sitter-heex.workspace = true
tree-sitter-json.workspace = true tree-sitter-json.workspace = true
@ -142,6 +143,7 @@ tree-sitter-nix.workspace = true
tree-sitter-nu.workspace = true tree-sitter-nu.workspace = true
tree-sitter-vue.workspace = true tree-sitter-vue.workspace = true
tree-sitter-uiua.workspace = true tree-sitter-uiua.workspace = true
tree-sitter-zig.workspace = true
url = "2.2" url = "2.2"
urlencoding = "2.1.2" urlencoding = "2.1.2"

View file

@ -7,11 +7,13 @@ use settings::Settings;
use std::{borrow::Cow, str, sync::Arc}; use std::{borrow::Cow, str, sync::Arc};
use util::{asset_str, paths::PLUGINS_DIR}; use util::{asset_str, paths::PLUGINS_DIR};
use self::elixir::ElixirSettings; use self::{deno::DenoSettings, elixir::ElixirSettings};
mod c; mod c;
mod css; mod css;
mod deno;
mod elixir; mod elixir;
mod gleam;
mod go; mod go;
mod haskell; mod haskell;
mod html; mod html;
@ -30,6 +32,7 @@ mod typescript;
mod uiua; mod uiua;
mod vue; mod vue;
mod yaml; mod yaml;
mod zig;
// 1. Add tree-sitter-{language} parser to zed crate // 1. Add tree-sitter-{language} parser to zed crate
// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below // 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below
@ -51,6 +54,7 @@ pub fn init(
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
ElixirSettings::register(cx); ElixirSettings::register(cx);
DenoSettings::register(cx);
let language = |name, grammar, adapters| { let language = |name, grammar, adapters| {
languages.register(name, load_config(name), grammar, adapters, load_queries) languages.register(name, load_config(name), grammar, adapters, load_queries)
@ -100,11 +104,21 @@ pub fn init(
), ),
} }
language(
"gleam",
tree_sitter_gleam::language(),
vec![Arc::new(gleam::GleamLspAdapter)],
);
language( language(
"go", "go",
tree_sitter_go::language(), tree_sitter_go::language(),
vec![Arc::new(go::GoLspAdapter)], vec![Arc::new(go::GoLspAdapter)],
); );
language(
"zig",
tree_sitter_zig::language(),
vec![Arc::new(zig::ZlsAdapter)],
);
language( language(
"heex", "heex",
tree_sitter_heex::language(), tree_sitter_heex::language(),
@ -135,32 +149,59 @@ pub fn init(
vec![Arc::new(rust::RustLspAdapter)], vec![Arc::new(rust::RustLspAdapter)],
); );
language("toml", tree_sitter_toml::language(), vec![]); language("toml", tree_sitter_toml::language(), vec![]);
language( match &DenoSettings::get(None, cx).enable {
"tsx", true => {
tree_sitter_typescript::language_tsx(), language(
vec![ "tsx",
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), tree_sitter_typescript::language_tsx(),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), vec![
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), Arc::new(deno::DenoLspAdapter::new()),
], Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
); ],
language( );
"typescript", language(
tree_sitter_typescript::language_typescript(), "typescript",
vec![ tree_sitter_typescript::language_typescript(),
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), vec![Arc::new(deno::DenoLspAdapter::new())],
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), );
], language(
); "javascript",
language( tree_sitter_typescript::language_tsx(),
"javascript", vec![
tree_sitter_typescript::language_tsx(), Arc::new(deno::DenoLspAdapter::new()),
vec![ Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), ],
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), );
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), }
], false => {
); language(
"tsx",
tree_sitter_typescript::language_tsx(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
],
);
language(
"typescript",
tree_sitter_typescript::language_typescript(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
);
language(
"javascript",
tree_sitter_typescript::language_tsx(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
],
);
}
}
language("haskell", tree_sitter_haskell::language(), vec![]); language("haskell", tree_sitter_haskell::language(), vec![]);
language( language(
"html", "html",

View file

@ -1,6 +1,6 @@
name = "Shell Script" name = "Shell Script"
path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"] path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
line_comment = "# " line_comments = ["# "]
first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
brackets = [ brackets = [
{ start = "[", end = "]", close = true, newline = false }, { start = "[", end = "]", close = true, newline = false },

View file

@ -15,7 +15,7 @@ pub struct CLspAdapter;
#[async_trait] #[async_trait]
impl super::LspAdapter for CLspAdapter { impl super::LspAdapter for CLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("clangd".into()) LanguageServerName("clangd".into())
} }

View file

@ -1,6 +1,6 @@
name = "C" name = "C"
path_suffixes = ["c"] path_suffixes = ["c"]
line_comment = "// " line_comments = ["// "]
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -1,6 +1,6 @@
name = "C++" name = "C++"
path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"]
line_comment = "// " line_comments = ["// "]
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -33,7 +33,7 @@ impl CssLspAdapter {
#[async_trait] #[async_trait]
impl LspAdapter for CssLspAdapter { impl LspAdapter for CssLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-css-language-server".into()) LanguageServerName("vscode-css-language-server".into())
} }
@ -91,7 +91,7 @@ impl LspAdapter for CssLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await get_cached_server_binary(container_dir, &*self.node).await
} }
async fn initialization_options(&self) -> Option<serde_json::Value> { fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({ Some(json!({
"provideFormatter": true "provideFormatter": true
})) }))

View file

@ -0,0 +1,223 @@
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use settings::Settings;
use smol::{fs, fs::File};
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
use util::{fs::remove_matching, github::latest_github_release};
use util::{github::GitHubLspBinaryVersion, ResultExt};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct DenoSettings {
pub enable: bool,
}
#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
pub struct DenoSettingsContent {
enable: Option<bool>,
}
impl Settings for DenoSettings {
const KEY: Option<&'static str> = Some("deno");
type FileContent = DenoSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
}
}
fn deno_server_binary_arguments() -> Vec<OsString> {
vec!["lsp".into()]
}
pub struct DenoLspAdapter {}
impl DenoLspAdapter {
pub fn new() -> Self {
DenoLspAdapter {}
}
}
#[async_trait]
impl LspAdapter for DenoLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("deno-language-server".into())
}
fn short_name(&self) -> &'static str {
"deno-ts"
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("denoland/deno", false, delegate.http_client()).await?;
let asset_name = format!("deno-{}-apple-darwin.zip", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("deno_{}.zip", version.name));
let version_dir = container_dir.join(format!("deno_{}", version.name));
let binary_path = version_dir.join("deno");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
let unzip_status = smol::process::Command::new("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.arg("-d")
.arg(&version_dir)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip deno archive"))?;
}
remove_matching(&container_dir, |entry| entry != version_dir).await;
}
Ok(LanguageServerBinary {
path: binary_path,
arguments: deno_server_binary_arguments(),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::SOURCE,
])
}
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
use lsp::CompletionItemKind as Kind;
let len = item.label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
_ => None,
}?;
let text = match &item.detail {
Some(detail) => format!("{} {}", item.label, detail),
None => item.label.clone(),
};
Some(language::CodeLabel {
text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
})
}
fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true,
}))
}
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([
("TypeScript".into(), "typescript".into()),
("JavaScript".into(), "javascript".into()),
("TSX".into(), "typescriptreact".into()),
])
}
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
match last {
Some(path) if path.is_dir() => {
let binary = path.join("deno");
if fs::metadata(&binary).await.is_ok() {
return Ok(LanguageServerBinary {
path: binary,
arguments: deno_server_binary_arguments(),
});
}
}
_ => {}
}
Err(anyhow!("no cached binary"))
})()
.await
.log_err()
}

View file

@ -67,7 +67,7 @@ pub struct ElixirLspAdapter;
#[async_trait] #[async_trait]
impl LspAdapter for ElixirLspAdapter { impl LspAdapter for ElixirLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("elixir-ls".into()) LanguageServerName("elixir-ls".into())
} }
@ -301,7 +301,7 @@ pub struct NextLspAdapter;
#[async_trait] #[async_trait]
impl LspAdapter for NextLspAdapter { impl LspAdapter for NextLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("next-ls".into()) LanguageServerName("next-ls".into())
} }
@ -452,7 +452,7 @@ pub struct LocalLspAdapter {
#[async_trait] #[async_trait]
impl LspAdapter for LocalLspAdapter { impl LspAdapter for LocalLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("local-ls".into()) LanguageServerName("local-ls".into())
} }

View file

@ -1,6 +1,6 @@
name = "Elixir" name = "Elixir"
path_suffixes = ["ex", "exs"] path_suffixes = ["ex", "exs"]
line_comment = "# " line_comments = ["# "]
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -1,6 +1,6 @@
name = "Elm" name = "Elm"
path_suffixes = ["elm"] path_suffixes = ["elm"]
line_comment = "-- " line_comments = ["-- "]
block_comment = ["{- ", " -}"] block_comment = ["{- ", " -}"]
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -0,0 +1,118 @@
use std::any::Any;
use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::io::BufReader;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use smol::fs;
use util::github::{latest_github_release, GitHubLspBinaryVersion};
use util::{async_maybe, ResultExt};
fn server_binary_arguments() -> Vec<OsString> {
vec!["lsp".into()]
}
pub struct GleamLspAdapter;
#[async_trait]
impl LspAdapter for GleamLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("gleam".into())
}
fn short_name(&self) -> &'static str {
"gleam"
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("gleam-lang/gleam", false, delegate.http_client()).await?;
let asset_name = format!(
"gleam-{version}-{arch}-apple-darwin.tar.gz",
version = release.name,
arch = std::env::consts::ARCH
);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let binary_path = container_dir.join("gleam");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(container_dir).await?;
}
Ok(LanguageServerBinary {
path: binary_path,
arguments: server_binary_arguments(),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["--version".into()];
binary
})
}
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
async_maybe!({
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
anyhow::Ok(LanguageServerBinary {
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
arguments: server_binary_arguments(),
})
})
.await
.log_err()
}

View file

@ -0,0 +1,10 @@
name = "Gleam"
path_suffixes = ["gleam"]
line_comments = ["// ", "/// "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
]

View file

@ -0,0 +1,130 @@
; Comments
(module_comment) @comment
(statement_comment) @comment
(comment) @comment
; Constants
(constant
name: (identifier) @constant)
; Modules
(module) @module
(import alias: (identifier) @module)
(remote_type_identifier
module: (identifier) @module)
(remote_constructor_name
module: (identifier) @module)
((field_access
record: (identifier) @module
field: (label) @function)
(#is-not? local))
; Functions
(unqualified_import (identifier) @function)
(unqualified_import "type" (type_identifier) @type)
(unqualified_import (type_identifier) @constructor)
(function
name: (identifier) @function)
(external_function
name: (identifier) @function)
(function_parameter
name: (identifier) @variable.parameter)
((function_call
function: (identifier) @function)
(#is-not? local))
((binary_expression
operator: "|>"
right: (identifier) @function)
(#is-not? local))
; "Properties"
; Assumed to be intended to refer to a name for a field; something that comes
; before ":" or after "."
; e.g. record field names, tuple indices, names for named arguments, etc
(label) @property
(tuple_access
index: (integer) @property)
; Attributes
(attribute
"@" @attribute
name: (identifier) @attribute)
(attribute_value (identifier) @constant)
; Type names
(remote_type_identifier) @type
(type_identifier) @type
; Data constructors
(constructor_name) @constructor
; Literals
(string) @string
((escape_sequence) @warning
; Deprecated in v0.33.0-rc2:
(#eq? @warning "\\e"))
(escape_sequence) @string.escape
(bit_string_segment_option) @function.builtin
(integer) @number
(float) @number
; Reserved identifiers
; TODO: when tree-sitter supports `#any-of?` in the Rust bindings,
; refactor this to use `#any-of?` rather than `#match?`
((identifier) @warning
(#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$"))
; Variables
(identifier) @variable
(discard) @comment.unused
; Keywords
[
(visibility_modifier) ; "pub"
(opacity_modifier) ; "opaque"
"as"
"assert"
"case"
"const"
; DEPRECATED: 'external' was removed in v0.30.
"external"
"fn"
"if"
"import"
"let"
"panic"
"todo"
"type"
"use"
] @keyword
; Operators
(binary_expression
operator: _ @operator)
(boolean_negation "!" @operator)
(integer_negation "-" @operator)
; Punctuation
[
"("
")"
"["
"]"
"{"
"}"
"<<"
">>"
] @punctuation.bracket
[
"."
","
;; Controversial -- maybe some are operators?
":"
"#"
"="
"->"
".."
"-"
"<-"
] @punctuation.delimiter

View file

@ -0,0 +1,4 @@
(function
(visibility_modifier)? @context
"fn" @context
name: (_) @name) @item

View file

@ -1,6 +1,6 @@
name = "GLSL" name = "GLSL"
path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"] path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
line_comment = "// " line_comments = ["// "]
block_comment = ["/* ", " */"] block_comment = ["/* ", " */"]
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -33,7 +33,7 @@ lazy_static! {
#[async_trait] #[async_trait]
impl super::LspAdapter for GoLspAdapter { impl super::LspAdapter for GoLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("gopls".into()) LanguageServerName("gopls".into())
} }

View file

@ -1,6 +1,6 @@
name = "Go" name = "Go"
path_suffixes = ["go"] path_suffixes = ["go"]
line_comment = "// " line_comments = ["// "]
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -33,7 +33,7 @@ impl HtmlLspAdapter {
#[async_trait] #[async_trait]
impl LspAdapter for HtmlLspAdapter { impl LspAdapter for HtmlLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-html-language-server".into()) LanguageServerName("vscode-html-language-server".into())
} }
@ -91,7 +91,7 @@ impl LspAdapter for HtmlLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await get_cached_server_binary(container_dir, &*self.node).await
} }
async fn initialization_options(&self) -> Option<serde_json::Value> { fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({ Some(json!({
"provideFormatter": true "provideFormatter": true
})) }))

View file

@ -1,5 +1,5 @@
name = "HTML" name = "HTML"
path_suffixes = ["html"] path_suffixes = ["html", "htm", "shtml"]
autoclose_before = ">})" autoclose_before = ">})"
block_comment = ["<!-- ", " -->"] block_comment = ["<!-- ", " -->"]
brackets = [ brackets = [

View file

@ -1,7 +1,7 @@
name = "JavaScript" name = "JavaScript"
path_suffixes = ["js", "jsx", "mjs", "cjs"] path_suffixes = ["js", "jsx", "mjs", "cjs"]
first_line_pattern = '^#!.*\bnode\b' first_line_pattern = '^#!.*\bnode\b'
line_comment = "// " line_comments = ["// "]
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },
@ -18,7 +18,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "babel" prettier_parser_name = "babel"
[overrides.element] [overrides.element]
line_comment = { remove = true } line_comments = { remove = true }
block_comment = ["{/* ", " */}"] block_comment = ["{/* ", " */}"]
[overrides.string] [overrides.string]

View file

@ -38,7 +38,7 @@ impl JsonLspAdapter {
#[async_trait] #[async_trait]
impl LspAdapter for JsonLspAdapter { impl LspAdapter for JsonLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("json-language-server".into()) LanguageServerName("json-language-server".into())
} }
@ -96,7 +96,7 @@ impl LspAdapter for JsonLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await get_cached_server_binary(container_dir, &*self.node).await
} }
async fn initialization_options(&self) -> Option<serde_json::Value> { fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({ Some(json!({
"provideFormatter": true "provideFormatter": true
})) }))
@ -140,7 +140,7 @@ impl LspAdapter for JsonLspAdapter {
}) })
} }
async fn language_ids(&self) -> HashMap<String, String> { fn language_ids(&self) -> HashMap<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect() [("JSON".into(), "jsonc".into())].into_iter().collect()
} }
} }

View file

@ -1,6 +1,6 @@
name = "JSON" name = "JSON"
path_suffixes = ["json"] path_suffixes = ["json"]
line_comment = "// " line_comments = ["// "]
autoclose_before = ",]}" autoclose_before = ",]}"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

View file

@ -18,7 +18,7 @@ pub struct LuaLspAdapter;
#[async_trait] #[async_trait]
impl super::LspAdapter for LuaLspAdapter { impl super::LspAdapter for LuaLspAdapter {
async fn name(&self) -> LanguageServerName { fn name(&self) -> LanguageServerName {
LanguageServerName("lua-language-server".into()) LanguageServerName("lua-language-server".into())
} }

View file

@ -1,6 +1,6 @@
name = "Lua" name = "Lua"
path_suffixes = ["lua"] path_suffixes = ["lua"]
line_comment = "-- " line_comments = ["-- "]
autoclose_before = ",]}" autoclose_before = ",]}"
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },

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