Merge branch 'main' into stream-git-statuses

This commit is contained in:
Mikayla Maki 2023-06-07 14:12:58 -07:00
commit 28ba27c9c5
No known key found for this signature in database
206 changed files with 5946 additions and 3086 deletions

View file

@ -93,7 +93,6 @@ jobs:
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
steps: steps:
- name: Install Rust - name: Install Rust
run: | run: |

View file

@ -6,8 +6,8 @@ on:
push: push:
branches: branches:
- randomized-tests-runner - randomized-tests-runner
schedule: # schedule:
- cron: '0 * * * *' # - cron: '0 * * * *'
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View file

@ -21,19 +21,3 @@ jobs:
${{ github.event.release.body }} ${{ github.event.release.body }}
``` ```
mixpanel_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r script/mixpanel_release/requirements.txt
- run: >
python script/mixpanel_release/main.py
${{ github.event.release.tag_name }}
${{ secrets.MIXPANEL_PROJECT_ID }}
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_USERNAME }}
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_SECRET }}

192
Cargo.lock generated
View file

@ -100,15 +100,24 @@ name = "ai"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets", "chrono",
"collections", "collections",
"editor", "editor",
"fs",
"futures 0.3.28", "futures 0.3.28",
"gpui", "gpui",
"isahc", "isahc",
"language",
"menu",
"schemars",
"search",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"theme",
"tiktoken-rs",
"util", "util",
"workspace",
] ]
[[package]] [[package]]
@ -180,15 +189,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.71" version = "1.0.71"
@ -219,15 +219,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "assets"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"rust-embed",
]
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.4.1" version = "0.4.1"
@ -404,7 +395,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -452,7 +443,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -495,7 +486,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -715,27 +706,42 @@ dependencies = [
[[package]] [[package]]
name = "bindgen" name = "bindgen"
version = "0.59.2" version = "0.65.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"clap 2.34.0",
"env_logger 0.9.3",
"lazy_static", "lazy_static",
"lazycell", "lazycell",
"log", "log",
"peeking_take_while", "peeking_take_while",
"prettyplease",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn 2.0.18",
"which", "which",
] ]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -861,6 +867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
dependencies = [ dependencies = [
"memchr", "memchr",
"once_cell",
"regex-automata",
"serde", "serde",
] ]
@ -1085,21 +1093,6 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim 0.8.0",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.25" version = "3.2.25"
@ -1112,9 +1105,9 @@ dependencies = [
"clap_lex", "clap_lex",
"indexmap", "indexmap",
"once_cell", "once_cell",
"strsim 0.10.0", "strsim",
"termcolor", "termcolor",
"textwrap 0.16.0", "textwrap",
] ]
[[package]] [[package]]
@ -1144,7 +1137,7 @@ name = "cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap 3.2.25", "clap",
"core-foundation", "core-foundation",
"core-services", "core-services",
"dirs 3.0.2", "dirs 3.0.2",
@ -1246,7 +1239,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.12.4" version = "0.13.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-tungstenite", "async-tungstenite",
@ -1254,7 +1247,7 @@ dependencies = [
"axum-extra", "axum-extra",
"base64 0.13.1", "base64 0.13.1",
"call", "call",
"clap 3.2.25", "clap",
"client", "client",
"collections", "collections",
"ctor", "ctor",
@ -1426,7 +1419,6 @@ name = "copilot_button"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets",
"context_menu", "context_menu",
"copilot", "copilot",
"editor", "editor",
@ -1797,7 +1789,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"scratch", "scratch",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -1814,7 +1806,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -2211,6 +2203,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fancy-regex"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
dependencies = [
"bit-set",
"regex",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -2588,7 +2590,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -4349,7 +4351,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -4794,6 +4796,16 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "prettyplease"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1"
dependencies = [
"proc-macro2",
"syn 2.0.18",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "0.1.5" version = "0.1.5"
@ -4829,9 +4841,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.56" version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -5108,9 +5120,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -6054,7 +6066,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -6097,7 +6109,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -6129,7 +6141,6 @@ name = "settings"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets",
"collections", "collections",
"fs", "fs",
"futures 0.3.28", "futures 0.3.28",
@ -6138,6 +6149,7 @@ dependencies = [
"lazy_static", "lazy_static",
"postage", "postage",
"pretty_assertions", "pretty_assertions",
"rust-embed",
"schemars", "schemars",
"serde", "serde",
"serde_derive", "serde_derive",
@ -6584,12 +6596,6 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -6661,9 +6667,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.15" version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -6849,15 +6855,6 @@ dependencies = [
"util", "util",
] ]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.16.0" version = "0.16.0"
@ -6931,7 +6928,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -6961,6 +6958,21 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "tiktoken-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4"
dependencies = [
"anyhow",
"base64 0.21.0",
"bstr",
"fancy-regex",
"lazy_static",
"parking_lot 0.12.1",
"rustc-hash",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.45" version = "0.1.45"
@ -7089,7 +7101,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -7277,7 +7289,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]
@ -7382,8 +7394,8 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-elixir" name = "tree-sitter-elixir"
version = "0.19.0" version = "0.1.0"
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter", "tree-sitter",
@ -7545,7 +7557,7 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-yaml" name = "tree-sitter-yaml"
version = "0.0.1" version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492" source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=5694b7f290cd9ef998829a0a6d8391a666370886#5694b7f290cd9ef998829a0a6d8391a666370886"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter", "tree-sitter",
@ -7779,6 +7791,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"rand 0.8.5", "rand 0.8.5",
"rust-embed",
"serde", "serde",
"serde_json", "serde_json",
"smol", "smol",
@ -7838,12 +7851,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -7855,7 +7862,6 @@ name = "vim"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets",
"async-compat", "async-compat",
"async-trait", "async-trait",
"collections", "collections",
@ -8007,7 +8013,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -8041,7 +8047,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -8683,7 +8689,6 @@ name = "workspace"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets",
"async-recursion 1.0.4", "async-recursion 1.0.4",
"bincode", "bincode",
"call", "call",
@ -8778,12 +8783,11 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.89.0" version = "0.91.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
"anyhow", "anyhow",
"assets",
"async-compression", "async-compression",
"async-recursion 0.3.2", "async-recursion 0.3.2",
"async-tar", "async-tar",
@ -8908,7 +8912,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.15", "syn 2.0.18",
] ]
[[package]] [[package]]

View file

@ -2,7 +2,6 @@
members = [ members = [
"crates/activity_indicator", "crates/activity_indicator",
"crates/ai", "crates/ai",
"crates/assets",
"crates/auto_update", "crates/auto_update",
"crates/breadcrumbs", "crates/breadcrumbs",
"crates/call", "crates/call",
@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] } postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
regex = { version = "1.5" } regex = { version = "1.5" }
rust-embed = { version = "6.3", features = ["include-exclude"] }
schemars = { version = "0.8" } schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@ -116,3 +116,4 @@ split-debuginfo = "unpacked"
[profile.release] [profile.release]
debug = true debug = true
lto = "thin"

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.65-bullseye as builder FROM rust:1.70-bullseye as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

View file

@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/> <path d="M6.01077 0.000234794C2.69085 0.000234794 0.000639074 2.18612 0.000639074 4.88385C0.000639074 6.0491 0.501914 7.11387 1.33823 7.95254C1.04475 9.13517 0.0640321 10.1894 0.0522927 10.2011C-0.00053487 10.2539 -0.0153266 10.3356 0.0170743 10.4061C0.0464229 10.4763 0.111459 10.5185 0.187766 10.5185C1.74324 10.5185 2.89019 9.77286 3.4889 9.31197C4.25431 9.60052 5.10894 9.76722 6.01053 9.76722C9.33045 9.76722 12 7.58063 12 4.88361C12 2.18659 9.33045 0 6.01053 0L6.01077 0.000234794Z" fill="#FAFAFA"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 609 B

Before After
Before After

View file

@ -16,6 +16,12 @@
"replace_newest": true "replace_newest": true
} }
], ],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-shift-down": "editor::AddSelectionBelow", "ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove", "ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine", "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",

View file

@ -185,13 +185,8 @@
], ],
"alt-\\": "copilot::Suggest", "alt-\\": "copilot::Suggest",
"alt-]": "copilot::NextSuggestion", "alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion" "alt-[": "copilot::PreviousSuggestion",
} "cmd->": "assistant::QuoteSelection"
},
{
"context": "Editor && extension == zmd",
"bindings": {
"cmd-enter": "ai::Assist"
} }
}, },
{ {
@ -201,6 +196,13 @@
"cmd-alt-enter": "editor::NewlineBelow" "cmd-alt-enter": "editor::NewlineBelow"
} }
}, },
{
"context": "AssistantEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd->": "assistant::QuoteSelection"
}
},
{ {
"context": "BufferSearchBar", "context": "BufferSearchBar",
"bindings": { "bindings": {
@ -250,12 +252,24 @@
"replace_newest": false "replace_newest": false
} }
], ],
"ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-k cmd-d": [ "cmd-k cmd-d": [
"editor::SelectNext", "editor::SelectNext",
{ {
"replace_newest": true "replace_newest": true
} }
], ],
"cmd-k ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"cmd-k cmd-i": "editor::Hover", "cmd-k cmd-i": "editor::Hover",
"cmd-/": [ "cmd-/": [
"editor::ToggleComments", "editor::ToggleComments",
@ -504,7 +518,7 @@
"terminal::SendText", "terminal::SendText",
"\u0001" "\u0001"
], ],
// Terminal.app compatability // Terminal.app compatibility
"alt-left": [ "alt-left": [
"terminal::SendText", "terminal::SendText",
"\u001bb" "\u001bb"

View file

@ -26,6 +26,12 @@
"replace_newest": false "replace_newest": false
} }
], ],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-/": [ "cmd-/": [
"editor::ToggleComments", "editor::ToggleComments",
{ {

View file

@ -81,6 +81,14 @@
// Default width of the project panel. // Default width of the project panel.
"default_width": 240 "default_width": 240
}, },
"assistant": {
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
"default_width": 450,
// Default height when the assistant is docked to the bottom.
"default_height": 320
},
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
// Whether to use language servers to provide code intelligence. // Whether to use language servers to provide code intelligence.
@ -245,7 +253,7 @@
// copy to the system clipboard. // copy to the system clipboard.
"copy_on_select": false, "copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's // Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values. // environment. Use `:` to separate multiple values.
"env": { "env": {
// "KEY": "value1:value2" // "KEY": "value1:value2"
}, },

View file

@ -0,0 +1,11 @@
// Folder-specific Zed settings
//
// A subset of Zed's settings can be configured on a per-folder basis.
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.
{}

View file

@ -1,7 +1,7 @@
// Zed settings // Folder-specific settings
// //
// For information on how to configure Zed, see the Zed // For a full list of overridable settings, and general information on folder-specific settings, see the documentation:
// documentation: https://zed.dev/docs/configuring-zed // https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
// //
// To see all of Zed's default settings without changing your // To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command // custom settings, run the `open default settings` command

View file

@ -9,17 +9,26 @@ path = "src/ai.rs"
doctest = false doctest = false
[dependencies] [dependencies]
assets = { path = "../assets"}
collections = { path = "../collections"} collections = { path = "../collections"}
editor = { path = "../editor" } editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" }
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true anyhow.workspace = true
chrono = "0.4"
futures.workspace = true futures.workspace = true
isahc.workspace = true isahc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
tiktoken-rs = "0.4"
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View file

@ -1,22 +1,10 @@
use anyhow::{anyhow, Result}; pub mod assistant;
use assets::Assets; mod assistant_settings;
use collections::HashMap;
use editor::Editor;
use futures::AsyncBufReadExt;
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
use gpui::executor::Background;
use gpui::{actions, AppContext, Task, ViewContext};
use isahc::prelude::*;
use isahc::{http::StatusCode, Request};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fs;
use std::rc::Rc;
use std::{io, sync::Arc};
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
use util::{ResultExt, TryFutureExt};
actions!(ai, [Assist]); pub use assistant::AssistantPanel;
use gpui::AppContext;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
// Data types for chat completion requests // Data types for chat completion requests
#[derive(Serialize)] #[derive(Serialize)]
@ -38,7 +26,7 @@ struct ResponseMessage {
content: Option<String>, content: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
enum Role { enum Role {
User, User,
@ -46,6 +34,26 @@ enum Role {
System, System,
} }
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct OpenAIResponseStreamEvent { struct OpenAIResponseStreamEvent {
pub id: Option<String>, pub id: Option<String>,
@ -86,228 +94,5 @@ struct OpenAIChoice {
} }
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
if *RELEASE_CHANNEL == ReleaseChannel::Stable { assistant::init(cx);
return;
}
let assistant = Rc::new(Assistant::default());
cx.add_action({
let assistant = assistant.clone();
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
assistant.assist(editor, cx).log_err();
}
});
cx.capture_action({
let assistant = assistant.clone();
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
if !assistant.cancel_last_assist(cx.view_id()) {
cx.propagate_action();
}
}
});
}
type CompletionId = usize;
#[derive(Default)]
struct Assistant(RefCell<AssistantState>);
#[derive(Default)]
struct AssistantState {
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
next_completion_id: CompletionId,
}
impl Assistant {
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
let api_key = std::env::var("OPENAI_API_KEY")?;
let selections = editor.selections.all(cx);
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
// Insert markers around selected text as described in the system prompt above.
let snapshot = buffer.snapshot(cx);
let mut user_message = String::new();
let mut user_message_suffix = String::new();
let mut buffer_offset = 0;
for selection in selections {
if !selection.is_empty() {
if user_message_suffix.is_empty() {
user_message_suffix.push_str("\n\n");
}
user_message_suffix.push_str("[Selected excerpt from above]\n");
user_message_suffix
.extend(snapshot.text_for_range(selection.start..selection.end));
user_message_suffix.push_str("\n\n");
}
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
user_message.push_str("[SELECTION_START]");
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
buffer_offset = selection.end;
user_message.push_str("[SELECTION_END]");
}
if buffer_offset < snapshot.len() {
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
}
user_message.push_str(&user_message_suffix);
// Ensure the document ends with 4 trailing newlines.
let trailing_newline_count = snapshot
.reversed_chars_at(snapshot.len())
.take_while(|c| *c == '\n')
.take(4);
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
(user_message, insertion_site)
});
let this = self.clone();
let buffer = editor.buffer().clone();
let executor = cx.background_executor().clone();
let editor_id = cx.view_id();
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
let assist_task = cx.spawn(|_, mut cx| {
async move {
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
let content = Assets::get("contexts/system.zmd").unwrap();
let mut system_message = std::str::from_utf8(content.data.as_ref())
.unwrap()
.to_string();
if let Ok(custom_system_message_path) =
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
{
system_message.push_str(
"\n\nAlso consider the following user-defined system prompt:\n\n",
);
// TODO: Replace this with our file system trait object.
system_message.push_str(
&cx.background()
.spawn(async move { fs::read_to_string(custom_system_message_path) })
.await?,
);
}
let stream = stream_completion(
api_key,
executor,
OpenAIRequest {
model: "gpt-4".to_string(),
messages: vec![
RequestMessage {
role: Role::System,
content: system_message.to_string(),
},
RequestMessage {
role: Role::User,
content: user_message,
},
],
stream: false,
},
);
let mut messages = stream.await?;
while let Some(message) = messages.next().await {
let mut message = message?;
if let Some(choice) = message.choices.pop() {
buffer.update(&mut cx, |buffer, cx| {
let text: Arc<str> = choice.delta.content?.into();
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
Some(())
});
}
}
this.0
.borrow_mut()
.assist_stacks
.get_mut(&editor_id)
.unwrap()
.retain(|(id, _)| *id != assist_id);
anyhow::Ok(())
}
.log_err()
});
self.0
.borrow_mut()
.assist_stacks
.entry(cx.view_id())
.or_default()
.push((assist_id, assist_task));
Ok(())
}
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
self.0
.borrow_mut()
.assist_stacks
.get_mut(&editor_id)
.and_then(|assists| assists.pop())
.is_some()
}
}
async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
tx.unbounded_send(event).log_err();
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
))
}
} }

1383
crates/ai/src/assistant.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
use anyhow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
Right,
Bottom,
}
#[derive(Deserialize, Debug)]
pub struct AssistantSettings {
pub dock: AssistantDockPosition,
pub default_width: f32,
pub default_height: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
pub dock: Option<AssistantDockPosition>,
pub default_width: Option<f32>,
pub default_height: Option<f32>,
}
impl Setting for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
type FileContent = AssistantSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View file

@ -1,14 +0,0 @@
[package]
name = "assets"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assets.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
anyhow.workspace = true
rust-embed = { version = "6.3", features = ["include-exclude"] }

View file

@ -1,29 +0,0 @@
use std::process::Command;
fn main() {
let output = Command::new("npm")
.current_dir("../../styles")
.args(["install", "--no-save"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"failed to install theme dependencies {}",
String::from_utf8_lossy(&output.stderr)
);
}
let output = Command::new("npm")
.current_dir("../../styles")
.args(["run", "build"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"build script failed {}",
String::from_utf8_lossy(&output.stderr)
);
}
println!("cargo:rerun-if-changed=../../styles/src");
}

View file

@ -159,10 +159,7 @@ impl Bundle {
fn path(&self) -> &Path { fn path(&self) -> &Path {
match self { match self {
Self::App { app_bundle, .. } => app_bundle, Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { Self::LocalPath { executable, .. } => executable,
executable: excutable,
..
} => excutable,
} }
} }

View file

@ -776,15 +776,6 @@ impl Client {
if credentials.is_none() && try_keychain { if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx); credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some(); read_from_keychain = credentials.is_some();
if read_from_keychain {
cx.read(|cx| {
self.telemetry().report_mixpanel_event(
"read credentials from keychain",
Default::default(),
*settings::get::<TelemetrySettings>(cx),
);
});
}
} }
if credentials.is_none() { if credentials.is_none() {
let mut status_rx = self.status(); let mut status_rx = self.status();
@ -1072,11 +1063,8 @@ impl Client {
) -> Task<Result<Credentials>> { ) -> Task<Result<Credentials>> {
let platform = cx.platform(); let platform = cx.platform();
let executor = cx.background(); let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone(); let http = self.http.clone();
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
executor.clone().spawn(async move { executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the // Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by // zed server to encrypt the user's access token, so that it can'be intercepted by
@ -1159,12 +1147,6 @@ impl Client {
.context("failed to decrypt access token")?; .context("failed to decrypt access token")?;
platform.activate(true); platform.activate(true);
telemetry.report_mixpanel_event(
"authenticate with browser",
Default::default(),
telemetry_settings,
);
Ok(Credentials { Ok(Credentials {
user_id: user_id.parse()?, user_id: user_id.parse()?,
access_token, access_token,

View file

@ -1,14 +1,9 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{executor::Background, serde_json, AppContext, Task};
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Serialize; use serde::Serialize;
use serde_json::json;
use std::{ use std::{
env, env,
io::Write, io::Write,
@ -19,7 +14,7 @@ use std::{
}; };
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use util::http::HttpClient; use util::http::HttpClient;
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; use util::{channel::ReleaseChannel, TryFutureExt};
use uuid::Uuid; use uuid::Uuid;
pub struct Telemetry { pub struct Telemetry {
@ -37,23 +32,15 @@ struct TelemetryState {
os_name: &'static str, os_name: &'static str,
os_version: Option<Arc<str>>, os_version: Option<Arc<str>>,
architecture: &'static str, architecture: &'static str,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>, clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
flush_mixpanel_events_task: Option<Task<()>>,
flush_clickhouse_events_task: Option<Task<()>>, flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>, log_file: Option<NamedTempFile>,
is_staff: Option<bool>, is_staff: Option<bool>,
} }
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events"; const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! { lazy_static! {
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
static ref CLICKHOUSE_EVENTS_URL: String = static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH); format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
} }
@ -95,47 +82,6 @@ pub enum ClickhouseEvent {
}, },
} }
#[derive(Serialize, Debug)]
struct MixpanelEvent {
event: String,
properties: MixpanelEventProperties,
}
#[derive(Serialize, Debug)]
struct MixpanelEventProperties {
// Mixpanel required fields
#[serde(skip_serializing_if = "str::is_empty")]
token: &'static str,
time: u128,
#[serde(rename = "distinct_id")]
installation_id: Option<Arc<str>>,
#[serde(rename = "$insert_id")]
insert_id: usize,
// Custom fields
#[serde(skip_serializing_if = "Option::is_none", flatten)]
event_properties: Option<Map<String, Value>>,
#[serde(rename = "OS Name")]
os_name: &'static str,
#[serde(rename = "OS Version")]
os_version: Option<Arc<str>>,
#[serde(rename = "Release Channel")]
release_channel: Option<&'static str>,
#[serde(rename = "App Version")]
app_version: Option<Arc<str>>,
#[serde(rename = "Signed In")]
signed_in: bool,
}
#[derive(Serialize)]
struct MixpanelEngageRequest {
#[serde(rename = "$token")]
token: &'static str,
#[serde(rename = "$distinct_id")]
installation_id: Arc<str>,
#[serde(rename = "$set")]
set: Value,
}
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 1; const MAX_QUEUE_LEN: usize = 1;
@ -168,29 +114,13 @@ impl Telemetry {
release_channel, release_channel,
installation_id: None, installation_id: None,
metrics_id: None, metrics_id: None,
mixpanel_events_queue: Default::default(),
clickhouse_events_queue: Default::default(), clickhouse_events_queue: Default::default(),
flush_mixpanel_events_task: Default::default(),
flush_clickhouse_events_task: Default::default(), flush_clickhouse_events_task: Default::default(),
next_mixpanel_event_id: 0,
log_file: None, log_file: None,
is_staff: None, is_staff: None,
}), }),
}); });
if MIXPANEL_TOKEN.is_some() {
this.executor
.spawn({
let this = this.clone();
async move {
if let Some(tempfile) = NamedTempFile::new().log_err() {
this.state.lock().log_file = Some(tempfile);
}
}
})
.detach();
}
this this
} }
@ -218,20 +148,9 @@ impl Telemetry {
let mut state = this.state.lock(); let mut state = this.state.lock();
state.installation_id = Some(installation_id.clone()); state.installation_id = Some(installation_id.clone());
for event in &mut state.mixpanel_events_queue {
event
.properties
.installation_id
.get_or_insert_with(|| installation_id.clone());
}
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_mixpanel_events { drop(state);
this.flush_mixpanel_events();
}
if has_clickhouse_events { if has_clickhouse_events {
this.flush_clickhouse_events(); this.flush_clickhouse_events();
@ -256,37 +175,11 @@ impl Telemetry {
return; return;
} }
let this = self.clone();
let mut state = self.state.lock(); let mut state = self.state.lock();
let installation_id = state.installation_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into()); let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone(); state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff); state.is_staff = Some(is_staff);
drop(state); drop(state);
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
self.executor
.spawn(
async move {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
installation_id,
set: json!({
"Staff": is_staff,
"ID": metrics_id,
"App": true
}),
}])?;
this.http_client
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
} }
pub fn report_clickhouse_event( pub fn report_clickhouse_event(
@ -310,7 +203,7 @@ impl Telemetry {
}); });
if state.installation_id.is_some() { if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN { if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state); drop(state);
self.flush_clickhouse_events(); self.flush_clickhouse_events();
} else { } else {
@ -324,55 +217,6 @@ impl Telemetry {
} }
} }
pub fn report_mixpanel_event(
self: &Arc<Self>,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics {
return;
}
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.into(),
properties: MixpanelEventProperties {
token: "",
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
installation_id: state.installation_id.clone(),
insert_id: post_inc(&mut state.next_mixpanel_event_id),
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
None
},
os_name: state.os_name,
os_version: state.os_version.clone(),
release_channel: state.release_channel,
app_version: state.app_version.clone(),
signed_in: state.metrics_id.is_some(),
},
};
state.mixpanel_events_queue.push(event);
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_mixpanel_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_mixpanel_events();
}));
}
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> { pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone() self.state.lock().metrics_id.clone()
} }
@ -385,44 +229,6 @@ impl Telemetry {
self.state.lock().is_staff self.state.lock().is_staff
} }
fn flush_mixpanel_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.mixpanel_events_queue);
state.flush_mixpanel_events_task.take();
drop(state);
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
event.properties.token = token;
}
}
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &events)?;
this.http_client
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}
fn flush_clickhouse_events(self: &Arc<Self>) { fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock(); let mut state = self.state.lock();
let mut events = mem::take(&mut state.clickhouse_events_queue); let mut events = mem::take(&mut state.clickhouse_events_queue);

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.12.4" version = "0.13.1"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -97,6 +97,17 @@ CREATE TABLE "worktree_repositories" (
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id"); CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"path" VARCHAR NOT NULL,
"content" TEXT,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" ( CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL, "project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL,

View file

@ -0,0 +1,10 @@
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"path" VARCHAR NOT NULL,
"content" TEXT NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");

View file

@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
mod worktree_entry; mod worktree_entry;
mod worktree_repository; mod worktree_repository;
mod worktree_repository_statuses; mod worktree_repository_statuses;
mod worktree_settings_file;
use crate::executor::Executor; use crate::executor::Executor;
use crate::{Error, Result}; use crate::{Error, Result};
@ -1494,6 +1495,7 @@ impl Database {
updated_repositories: Default::default(), updated_repositories: Default::default(),
removed_repositories: Default::default(), removed_repositories: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64, scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64,
}; };
@ -1591,6 +1593,25 @@ impl Database {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees
.iter_mut()
.find(|w| w.id == db_settings_file.worktree_id as u64)
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
let mut collaborators = project let mut collaborators = project
.find_related(project_collaborator::Entity) .find_related(project_collaborator::Entity)
.all(&*tx) .all(&*tx)
@ -2530,6 +2551,58 @@ impl Database {
.await .await
} }
pub async fn update_worktree_settings(
&self,
update: &proto::UpdateWorktreeSettings,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
if let Some(content) = &update.content {
worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
content: ActiveValue::Set(content.clone()),
})
.on_conflict(
OnConflict::columns([
worktree_settings_file::Column::ProjectId,
worktree_settings_file::Column::WorktreeId,
worktree_settings_file::Column::Path,
])
.update_column(worktree_settings_file::Column::Content)
.to_owned(),
)
.exec(&*tx)
.await?;
} else {
worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
pub async fn join_project( pub async fn join_project(
&self, &self,
project_id: ProjectId, project_id: ProjectId,
@ -2600,6 +2673,7 @@ impl Database {
entries: Default::default(), entries: Default::default(),
repository_entries: Default::default(), repository_entries: Default::default(),
diagnostic_summaries: Default::default(), diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64, scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64, completed_scan_id: db_worktree.completed_scan_id as u64,
}, },
@ -2684,6 +2758,25 @@ impl Database {
} }
} }
// Populate worktree settings files
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) =
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
// Populate language servers. // Populate language servers.
let language_servers = project let language_servers = project
.find_related(language_server::Entity) .find_related(language_server::Entity)
@ -3347,6 +3440,7 @@ pub struct RejoinedWorktree {
pub updated_repositories: Vec<proto::RepositoryEntry>, pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>, pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>, pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64, pub scan_id: u64,
pub completed_scan_id: u64, pub completed_scan_id: u64,
} }
@ -3402,10 +3496,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>, pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>, pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>, pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64, pub scan_id: u64,
pub completed_scan_id: u64, pub completed_scan_id: u64,
} }
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
}
#[cfg(test)] #[cfg(test)]
pub use test::*; pub use test::*;

View file

@ -0,0 +1,19 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_settings_files")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub content: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -200,6 +200,7 @@ impl Server {
.add_message_handler(start_language_server) .add_message_handler(start_language_server)
.add_message_handler(update_language_server) .add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary) .add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
.add_request_handler(forward_project_request::<proto::GetHover>) .add_request_handler(forward_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>) .add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>) .add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@ -1088,6 +1089,18 @@ async fn rejoin_room(
}, },
)?; )?;
} }
for settings_file in worktree.settings_files {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project.id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
} }
for language_server in &project.language_servers { for language_server in &project.language_servers {
@ -1410,6 +1423,18 @@ async fn join_project(
}, },
)?; )?;
} }
for settings_file in worktree.settings_files {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project_id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
} }
for language_server in &project.language_servers { for language_server in &project.language_servers {
@ -1525,6 +1550,29 @@ async fn update_diagnostic_summary(
Ok(()) Ok(())
} }
async fn update_worktree_settings(
message: proto::UpdateWorktreeSettings,
session: Session,
) -> Result<()> {
let guest_connection_ids = session
.db()
.await
.update_worktree_settings(&message, session.connection_id)
.await?;
broadcast(
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, message.clone())
},
);
Ok(())
}
async fn start_language_server( async fn start_language_server(
request: proto::StartLanguageServer, request: proto::StartLanguageServer,
session: Session, session: Session,

View file

@ -3090,6 +3090,135 @@ async fn test_fs_operations(
}); });
} }
#[gpui::test(iterations = 10)]
async fn test_local_settings(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// As client A, open a project that contains some local settings files
client_a
.fs
.insert_tree(
"/dir",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 2 }"#
},
"a": {
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#
},
"a.txt": "a-contents",
},
"b": {
"b.txt": "b-contents",
}
}),
)
.await;
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// As client B, join that project and observe the local settings.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, update a settings file. As Client B, see the changed settings.
client_a
.fs
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, create and remove some settings files. As client B, see the changed settings.
client_a
.fs
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
client_a
.fs
.create_dir("/dir/b/.zed".as_ref())
.await
.unwrap();
client_a
.fs
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
]
)
});
// As client B, disconnect.
server.forbid_connections();
server.disconnect_client(client_b.peer_id().unwrap());
// As client A, change and remove settings files while client B is disconnected.
client_a
.fs
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
.await;
client_a
.fs
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
deterministic.run_until_parked();
// As client B, reconnect and see the changed settings.
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT);
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
)
});
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save( async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,

View file

@ -472,7 +472,7 @@ impl CollabTitlebarItem {
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| { MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations //TODO: Ensure this button has consistent width for both text variations
let style = titlebar.share_button.style_for(state, false); let style = titlebar.share_button.style_for(state, false);
Label::new(label, style.text.clone()) Label::new(label, style.text.clone())
.contained() .contained()

View file

@ -4,7 +4,7 @@ mod sign_in;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use collections::HashMap; use collections::{HashMap, HashSet};
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{ use gpui::{
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
@ -127,7 +127,7 @@ impl CopilotServer {
struct RunningCopilotServer { struct RunningCopilotServer {
lsp: Arc<LanguageServer>, lsp: Arc<LanguageServer>,
sign_in_status: SignInStatus, sign_in_status: SignInStatus,
registered_buffers: HashMap<u64, RegisteredBuffer>, registered_buffers: HashMap<usize, RegisteredBuffer>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -163,7 +163,6 @@ impl Status {
} }
struct RegisteredBuffer { struct RegisteredBuffer {
id: u64,
uri: lsp::Url, uri: lsp::Url,
language_id: String, language_id: String,
snapshot: BufferSnapshot, snapshot: BufferSnapshot,
@ -178,13 +177,13 @@ impl RegisteredBuffer {
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Copilot>, cx: &mut ModelContext<Copilot>,
) -> oneshot::Receiver<(i32, BufferSnapshot)> { ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
let id = self.id;
let (done_tx, done_rx) = oneshot::channel(); let (done_tx, done_rx) = oneshot::channel();
if buffer.read(cx).version() == self.snapshot.version { if buffer.read(cx).version() == self.snapshot.version {
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
} else { } else {
let buffer = buffer.downgrade(); let buffer = buffer.downgrade();
let id = buffer.id();
let prev_pending_change = let prev_pending_change =
mem::replace(&mut self.pending_buffer_change, Task::ready(None)); mem::replace(&mut self.pending_buffer_change, Task::ready(None));
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move { self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
@ -268,7 +267,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>, node_runtime: Arc<NodeRuntime>,
server: CopilotServer, server: CopilotServer,
buffers: HashMap<u64, WeakModelHandle<Buffer>>, buffers: HashSet<WeakModelHandle<Buffer>>,
} }
impl Entity for Copilot { impl Entity for Copilot {
@ -318,7 +317,7 @@ impl Copilot {
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
let http = self.http.clone(); let http = self.http.clone();
let node_runtime = self.node_runtime.clone(); let node_runtime = self.node_runtime.clone();
if all_language_settings(cx).copilot_enabled(None, None) { if all_language_settings(None, cx).copilot_enabled(None, None) {
if matches!(self.server, CopilotServer::Disabled) { if matches!(self.server, CopilotServer::Disabled) {
let start_task = cx let start_task = cx
.spawn({ .spawn({
@ -375,7 +374,7 @@ impl Copilot {
server server
.on_notification::<LogMessage, _>(|params, _cx| { .on_notification::<LogMessage, _>(|params, _cx| {
match params.level { match params.level {
// Copilot is pretty agressive about logging // Copilot is pretty aggressive about logging
0 => debug!("copilot: {}", params.message), 0 => debug!("copilot: {}", params.message),
1 => debug!("copilot: {}", params.message), 1 => debug!("copilot: {}", params.message),
_ => error!("copilot: {}", params.message), _ => error!("copilot: {}", params.message),
@ -559,8 +558,8 @@ impl Copilot {
} }
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) { pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
let buffer_id = buffer.read(cx).remote_id(); let weak_buffer = buffer.downgrade();
self.buffers.insert(buffer_id, buffer.downgrade()); self.buffers.insert(weak_buffer.clone());
if let CopilotServer::Running(RunningCopilotServer { if let CopilotServer::Running(RunningCopilotServer {
lsp: server, lsp: server,
@ -573,8 +572,7 @@ impl Copilot {
return; return;
} }
let buffer_id = buffer.read(cx).remote_id(); registered_buffers.entry(buffer.id()).or_insert_with(|| {
registered_buffers.entry(buffer_id).or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx); let uri: lsp::Url = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language()); let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
@ -592,7 +590,6 @@ impl Copilot {
.log_err(); .log_err();
RegisteredBuffer { RegisteredBuffer {
id: buffer_id,
uri, uri,
language_id, language_id,
snapshot, snapshot,
@ -603,8 +600,8 @@ impl Copilot {
this.handle_buffer_event(buffer, event, cx).log_err(); this.handle_buffer_event(buffer, event, cx).log_err();
}), }),
cx.observe_release(buffer, move |this, _buffer, _cx| { cx.observe_release(buffer, move |this, _buffer, _cx| {
this.buffers.remove(&buffer_id); this.buffers.remove(&weak_buffer);
this.unregister_buffer(buffer_id); this.unregister_buffer(&weak_buffer);
}), }),
], ],
} }
@ -619,8 +616,7 @@ impl Copilot {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<()> { ) -> Result<()> {
if let Ok(server) = self.server.as_running() { if let Ok(server) = self.server.as_running() {
let buffer_id = buffer.read(cx).remote_id(); if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
match event { match event {
language::Event::Edited => { language::Event::Edited => {
let _ = registered_buffer.report_changes(&buffer, cx); let _ = registered_buffer.report_changes(&buffer, cx);
@ -674,9 +670,9 @@ impl Copilot {
Ok(()) Ok(())
} }
fn unregister_buffer(&mut self, buffer_id: u64) { fn unregister_buffer(&mut self, buffer: &WeakModelHandle<Buffer>) {
if let Ok(server) = self.server.as_running() { if let Ok(server) = self.server.as_running() {
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) { if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) {
server server
.lsp .lsp
.notify::<lsp::notification::DidCloseTextDocument>( .notify::<lsp::notification::DidCloseTextDocument>(
@ -779,16 +775,12 @@ impl Copilot {
Err(error) => return Task::ready(Err(error)), Err(error) => return Task::ready(Err(error)),
}; };
let lsp = server.lsp.clone(); let lsp = server.lsp.clone();
let buffer_id = buffer.read(cx).remote_id(); let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
let snapshot = registered_buffer.report_changes(buffer, cx); let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone(); let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer); let position = position.to_point_utf16(buffer);
let settings = language_settings( let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
buffer.language_at(position).map(|l| l.name()).as_deref(),
cx,
);
let tab_size = settings.tab_size; let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs; let hard_tabs = settings.hard_tabs;
let relative_path = buffer let relative_path = buffer
@ -853,7 +845,7 @@ impl Copilot {
lsp_status: request::SignInStatus, lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
self.buffers.retain(|_, buffer| buffer.is_upgradable(cx)); self.buffers.retain(|buffer| buffer.is_upgradable(cx));
if let Ok(server) = self.server.as_running() { if let Ok(server) = self.server.as_running() {
match lsp_status { match lsp_status {
@ -861,7 +853,7 @@ impl Copilot {
| request::SignInStatus::MaybeOk { .. } | request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => { | request::SignInStatus::AlreadySignedIn { .. } => {
server.sign_in_status = SignInStatus::Authorized; server.sign_in_status = SignInStatus::Authorized;
for buffer in self.buffers.values().cloned().collect::<Vec<_>>() { for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
if let Some(buffer) = buffer.upgrade(cx) { if let Some(buffer) = buffer.upgrade(cx) {
self.register_buffer(&buffer, cx); self.register_buffer(&buffer, cx);
} }
@ -869,14 +861,14 @@ impl Copilot {
} }
request::SignInStatus::NotAuthorized { .. } => { request::SignInStatus::NotAuthorized { .. } => {
server.sign_in_status = SignInStatus::Unauthorized; server.sign_in_status = SignInStatus::Unauthorized;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() { for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id); self.unregister_buffer(&buffer);
} }
} }
request::SignInStatus::NotSignedIn => { request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut; server.sign_in_status = SignInStatus::SignedOut;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() { for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id); self.unregister_buffer(&buffer);
} }
} }
} }
@ -899,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx)).unwrap() lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
} else { } else {
format!("buffer://{}", buffer.read(cx).remote_id()) format!("buffer://{}", buffer.id()).parse().unwrap()
.parse()
.unwrap()
} }
} }
@ -1175,6 +1165,10 @@ mod tests {
fn to_proto(&self) -> rpc::proto::File { fn to_proto(&self) -> rpc::proto::File {
unimplemented!() unimplemented!()
} }
fn worktree_id(&self) -> usize {
0
}
} }
impl language::LocalFile for File { impl language::LocalFile for File {

View file

@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
doctest = false doctest = false
[dependencies] [dependencies]
assets = { path = "../assets" }
copilot = { path = "../copilot" } copilot = { path = "../copilot" }
editor = { path = "../editor" } editor = { path = "../editor" }
fs = { path = "../fs" } fs = { path = "../fs" }

View file

@ -9,7 +9,10 @@ use gpui::{
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
}; };
use language::language_settings::{self, all_language_settings, AllLanguageSettings}; use language::{
language_settings::{self, all_language_settings, AllLanguageSettings},
File, Language,
};
use settings::{update_settings_file, SettingsStore}; use settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::{paths, ResultExt}; use util::{paths, ResultExt};
@ -26,8 +29,8 @@ pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>, popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>, editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>, editor_enabled: Option<bool>,
language: Option<Arc<str>>, language: Option<Arc<Language>>,
path: Option<Arc<Path>>, file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
} }
@ -41,7 +44,7 @@ impl View for CopilotButton {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let all_language_settings = &all_language_settings(cx); let all_language_settings = all_language_settings(None, cx);
if !all_language_settings.copilot.feature_enabled { if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any(); return Empty::new().into_any();
} }
@ -165,7 +168,7 @@ impl CopilotButton {
editor_subscription: None, editor_subscription: None,
editor_enabled: None, editor_enabled: None,
language: None, language: None,
path: None, file: None,
fs, fs,
} }
} }
@ -197,14 +200,13 @@ impl CopilotButton {
if let Some(language) = self.language.clone() { if let Some(language) = self.language.clone() {
let fs = fs.clone(); let fs = fs.clone();
let language_enabled = let language_enabled = language_settings::language_settings(Some(&language), None, cx)
language_settings::language_settings(Some(language.as_ref()), cx)
.show_copilot_suggestions; .show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
format!( format!(
"{} Suggestions for {}", "{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" }, if language_enabled { "Hide" } else { "Show" },
language language.name()
), ),
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
)); ));
@ -212,9 +214,9 @@ impl CopilotButton {
let settings = settings::get::<AllLanguageSettings>(cx); let settings = settings::get::<AllLanguageSettings>(cx);
if let Some(path) = self.path.as_ref() { if let Some(file) = &self.file {
let path_enabled = settings.copilot_enabled_for_path(path); let path = file.path().clone();
let path = path.clone(); let path_enabled = settings.copilot_enabled_for_path(&path);
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
format!( format!(
"{} Suggestions for This Path", "{} Suggestions for This Path",
@ -276,17 +278,15 @@ impl CopilotButton {
let editor = editor.read(cx); let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start; let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot let language = snapshot.language_at(suggestion_anchor);
.language_at(suggestion_anchor) let file = snapshot.file_at(suggestion_anchor).cloned();
.map(|language| language.name());
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
self.editor_enabled = Some( self.editor_enabled = Some(
all_language_settings(cx) all_language_settings(self.file.as_ref(), cx)
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
); );
self.language = language_name; self.language = language.cloned();
self.path = path.cloned(); self.file = file;
cx.notify() cx.notify()
} }
@ -315,9 +315,7 @@ async fn configure_disabled_globs(
let settings_editor = workspace let settings_editor = workspace
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || { create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content(&assets::Assets) settings::initial_user_settings_content().as_ref().into()
.as_ref()
.into()
}) })
})? })?
.await? .await?
@ -363,17 +361,18 @@ async fn configure_disabled_globs(
} }
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) { fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None); let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
}); });
} }
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) { fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None); let show_copilot_suggestions =
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages file.languages
.entry(language) .entry(language.name())
.or_default() .or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions); .show_copilot_suggestions = Some(!show_copilot_suggestions);
}); });

View file

@ -272,12 +272,11 @@ impl DisplayMap {
} }
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 { fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language_name = buffer let language = buffer
.read(cx) .read(cx)
.as_singleton() .as_singleton()
.and_then(|buffer| buffer.read(cx).language()) .and_then(|buffer| buffer.read(cx).language());
.map(|language| language.name()); language_settings(language.as_deref(), None, cx).tab_size
language_settings(language_name.as_deref(), cx).tab_size
} }
#[cfg(test)] #[cfg(test)]
@ -476,7 +475,7 @@ impl DisplaySnapshot {
}) })
} }
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from` /// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
/// Stops if `condition` returns false for any of the character position pairs observed. /// Stops if `condition` returns false for any of the character position pairs observed.
pub fn find_while<'a>( pub fn find_while<'a>(
&'a self, &'a self,
@ -487,7 +486,7 @@ impl DisplaySnapshot {
Self::find_internal(self.chars_at(from), target.chars().collect(), condition) Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
} }
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from` /// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
/// Stops if `condition` returns false for any of the character position pairs observed. /// Stops if `condition` returns false for any of the character position pairs observed.
pub fn reverse_find_while<'a>( pub fn reverse_find_while<'a>(
&'a self, &'a self,

View file

@ -10,7 +10,7 @@ pub mod items;
mod link_go_to_definition; mod link_go_to_definition;
mod mouse_context_menu; mod mouse_context_menu;
pub mod movement; pub mod movement;
mod multi_buffer; pub mod multi_buffer;
mod persistence; mod persistence;
pub mod scroll; pub mod scroll;
pub mod selections_collection; pub mod selections_collection;
@ -31,9 +31,13 @@ use copilot::Copilot;
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
use display_map::*; use display_map::*;
pub use editor_settings::EditorSettings; pub use editor_settings::EditorSettings;
pub use element::*; pub use element::RenderExcerptHeaderParams;
pub use element::{
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
};
use futures::FutureExt; use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::LayoutContext;
use gpui::{ use gpui::{
actions, actions,
color::Color, color::Color,
@ -44,9 +48,9 @@ use gpui::{
impl_actions, impl_actions,
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
serde_json::{self, json}, serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, WindowContext,
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
@ -109,6 +113,12 @@ pub struct SelectNext {
pub replace_newest: bool, pub replace_newest: bool,
} }
#[derive(Clone, Deserialize, PartialEq, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(Clone, Deserialize, PartialEq)] #[derive(Clone, Deserialize, PartialEq)]
pub struct SelectToBeginningOfLine { pub struct SelectToBeginningOfLine {
#[serde(default)] #[serde(default)]
@ -270,6 +280,7 @@ impl_actions!(
editor, editor,
[ [
SelectNext, SelectNext,
SelectPrevious,
SelectToBeginningOfLine, SelectToBeginningOfLine,
SelectToEndOfLine, SelectToEndOfLine,
ToggleCodeActions, ToggleCodeActions,
@ -365,6 +376,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::add_selection_above); cx.add_action(Editor::add_selection_above);
cx.add_action(Editor::add_selection_below); cx.add_action(Editor::add_selection_below);
cx.add_action(Editor::select_next); cx.add_action(Editor::select_next);
cx.add_action(Editor::select_previous);
cx.add_action(Editor::toggle_comments); cx.add_action(Editor::toggle_comments);
cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_larger_syntax_node);
cx.add_action(Editor::select_smaller_syntax_node); cx.add_action(Editor::select_smaller_syntax_node);
@ -482,6 +494,7 @@ pub struct Editor {
columnar_selection_tail: Option<Anchor>, columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>, add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>, select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
selection_history: SelectionHistory, selection_history: SelectionHistory,
autoclose_regions: Vec<AutocloseRegion>, autoclose_regions: Vec<AutocloseRegion>,
snippet_stack: InvalidationStack<SnippetState>, snippet_stack: InvalidationStack<SnippetState>,
@ -496,7 +509,9 @@ pub struct Editor {
blink_manager: ModelHandle<BlinkManager>, blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool, show_local_selections: bool,
mode: EditorMode, mode: EditorMode,
show_gutter: bool,
placeholder_text: Option<Arc<str>>, placeholder_text: Option<Arc<str>>,
render_excerpt_header: Option<element::RenderExcerptHeader>,
highlighted_rows: Option<Range<u32>>, highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>, background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
@ -526,6 +541,7 @@ pub struct Editor {
pub struct EditorSnapshot { pub struct EditorSnapshot {
pub mode: EditorMode, pub mode: EditorMode,
pub show_gutter: bool,
pub display_snapshot: DisplaySnapshot, pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>, pub placeholder_text: Option<Arc<str>>,
is_focused: bool, is_focused: bool,
@ -537,6 +553,7 @@ pub struct EditorSnapshot {
struct SelectionHistoryEntry { struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>, selections: Arc<[Selection<Anchor>]>,
select_next_state: Option<SelectNextState>, select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
add_selections_state: Option<AddSelectionsState>, add_selections_state: Option<AddSelectionsState>,
} }
@ -1284,6 +1301,7 @@ impl Editor {
columnar_selection_tail: None, columnar_selection_tail: None,
add_selections_state: None, add_selections_state: None,
select_next_state: None, select_next_state: None,
select_prev_state: None,
selection_history: Default::default(), selection_history: Default::default(),
autoclose_regions: Default::default(), autoclose_regions: Default::default(),
snippet_stack: Default::default(), snippet_stack: Default::default(),
@ -1297,7 +1315,9 @@ impl Editor {
blink_manager: blink_manager.clone(), blink_manager: blink_manager.clone(),
show_local_selections: true, show_local_selections: true,
mode, mode,
show_gutter: mode == EditorMode::Full,
placeholder_text: None, placeholder_text: None,
render_excerpt_header: None,
highlighted_rows: None, highlighted_rows: None,
background_highlights: Default::default(), background_highlights: Default::default(),
nav_history: None, nav_history: None,
@ -1393,6 +1413,7 @@ impl Editor {
pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot { pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
EditorSnapshot { EditorSnapshot {
mode: self.mode, mode: self.mode,
show_gutter: self.show_gutter,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(), scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(), ongoing_scroll: self.scroll_manager.ongoing_scroll(),
@ -1505,6 +1526,7 @@ impl Editor {
let buffer = &display_map.buffer_snapshot; let buffer = &display_map.buffer_snapshot;
self.add_selections_state = None; self.add_selections_state = None;
self.select_next_state = None; self.select_next_state = None;
self.select_prev_state = None;
self.select_larger_syntax_node_stack.clear(); self.select_larger_syntax_node_stack.clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
self.snippet_stack self.snippet_stack
@ -2157,9 +2179,10 @@ impl Editor {
indent.len = cmp::min(indent.len, start_point.column); indent.len = cmp::min(indent.len, start_point.column);
let start = selection.start; let start = selection.start;
let end = selection.end; let end = selection.end;
let is_cursor = start == end;
let mut insert_extra_newline = false; let language_scope = buffer.language_scope_at(start);
if let Some(language) = buffer.language_scope_at(start) { let (comment_delimiter, insert_extra_newline) =
if let Some(language) = &language_scope {
let leading_whitespace_len = buffer let leading_whitespace_len = buffer
.reversed_chars_at(start) .reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n') .take_while(|c| c.is_whitespace() && *c != '\n')
@ -2172,25 +2195,56 @@ impl Editor {
.map(|c| c.len_utf8()) .map(|c| c.len_utf8())
.sum::<usize>(); .sum::<usize>();
insert_extra_newline = language.brackets().any(|(pair, enabled)| { let insert_extra_newline =
language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end(); let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start(); let pair_end = pair.end.trim_start();
enabled enabled
&& pair.newline && pair.newline
&& buffer && buffer.contains_str_at(
.contains_str_at(end + trailing_whitespace_len, pair_end) end + trailing_whitespace_len,
pair_end,
)
&& buffer.contains_str_at( && buffer.contains_str_at(
(start - leading_whitespace_len) (start - leading_whitespace_len)
.saturating_sub(pair_start.len()), .saturating_sub(pair_start.len()),
pair_start, pair_start,
) )
}); });
} // Comment extension on newline is allowed only for cursor selections
let comment_delimiter =
language.line_comment_prefix().filter(|_| is_cursor);
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
buffer
.buffer_line_for_row(start_point.row)
.is_some_and(|(snapshot, range)| {
snapshot
.chars_for_range(range)
.skip_while(|c| c.is_whitespace())
.take(delimiter.len())
.eq(delimiter.chars())
})
.then(|| delimiter.clone())
} else {
None
};
(comment_delimiter, insert_extra_newline)
} else {
(None, false)
};
let mut new_text = String::with_capacity(1 + indent.len as usize); let capacity_for_delimiter = comment_delimiter
new_text.push('\n'); .as_deref()
.map(str::len)
.unwrap_or_default();
let mut new_text =
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
new_text.push_str("\n");
new_text.extend(indent.chars()); new_text.extend(indent.chars());
if let Some(delimiter) = &comment_delimiter {
new_text.push_str(&delimiter);
}
if insert_extra_newline { if insert_extra_newline {
new_text = new_text.repeat(2); new_text = new_text.repeat(2);
} }
@ -2372,7 +2426,7 @@ impl Editor {
old_selections old_selections
.iter() .iter()
.map(|s| { .map(|s| {
let anchor = snapshot.anchor_after(s.end); let anchor = snapshot.anchor_after(s.head());
s.map(|_| anchor) s.map(|_| anchor)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -2525,7 +2579,7 @@ impl Editor {
.read(cx) .read(cx)
.text_anchor_for_position(position.clone(), cx)?; .text_anchor_for_position(position.clone(), cx)?;
// OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances, // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
// hence we do LSP request & edit on host side only — add formats to host's history. // hence we do LSP request & edit on host side only — add formats to host's history.
let push_to_lsp_host_history = true; let push_to_lsp_host_history = true;
// If this is not the host, append its history with new edits. // If this is not the host, append its history with new edits.
@ -3207,12 +3261,10 @@ impl Editor {
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
let path = snapshot.file_at(location).map(|file| file.path().as_ref()); let file = snapshot.file_at(location);
let language_name = snapshot let language = snapshot.language_at(location);
.language_at(location) let settings = all_language_settings(file, cx);
.map(|language| language.name()); settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
let settings = all_language_settings(cx);
settings.copilot_enabled(language_name.as_deref(), path)
} }
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@ -3549,7 +3601,9 @@ impl Editor {
s.move_with(|map, selection| { s.move_with(|map, selection| {
if selection.is_empty() && !line_mode { if selection.is_empty() && !line_mode {
let cursor = movement::right(map, selection.head()); let cursor = movement::right(map, selection.head());
selection.set_head(cursor, SelectionGoal::None); selection.end = cursor;
selection.reversed = true;
selection.goal = SelectionGoal::None;
} }
}) })
}); });
@ -5213,6 +5267,101 @@ impl Editor {
} }
} }
pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
self.push_to_selection_history();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let mut selections = self.selections.all::<usize>(cx);
if let Some(mut select_prev_state) = self.select_prev_state.take() {
let query = &select_prev_state.query;
if !select_prev_state.done {
let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
let mut next_selected_range = None;
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
let bytes_before_last_selection =
buffer.reversed_bytes_in_range(0..last_selection.start);
let bytes_after_first_selection =
buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
let query_matches = query
.stream_find_iter(bytes_before_last_selection)
.map(|result| (last_selection.start, result))
.chain(
query
.stream_find_iter(bytes_after_first_selection)
.map(|result| (buffer.len(), result)),
);
for (end_offset, query_match) in query_matches {
let query_match = query_match.unwrap(); // can only fail due to I/O
let offset_range =
end_offset - query_match.end()..end_offset - query_match.start();
let display_range = offset_range.start.to_display_point(&display_map)
..offset_range.end.to_display_point(&display_map);
if !select_prev_state.wordwise
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
next_selected_range = Some(offset_range);
break;
}
}
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(next_selected_range);
});
} else {
select_prev_state.done = true;
}
}
self.select_prev_state = Some(select_prev_state);
} else if selections.len() == 1 {
let selection = selections.last_mut().unwrap();
if selection.start == selection.end {
let word_range = movement::surrounding_word(
&display_map,
selection.start.to_display_point(&display_map),
);
selection.start = word_range.start.to_offset(&display_map, Bias::Left);
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
selection.goal = SelectionGoal::None;
selection.reversed = false;
let query = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
let query = query.chars().rev().collect::<String>();
let select_state = SelectNextState {
query: AhoCorasick::new_auto_configured(&[query]),
wordwise: true,
done: false,
};
self.unfold_ranges([selection.start..selection.end], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select(selections);
});
self.select_prev_state = Some(select_state);
} else {
let query = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
let query = query.chars().rev().collect::<String>();
self.select_prev_state = Some(SelectNextState {
query: AhoCorasick::new_auto_configured(&[query]),
wordwise: false,
done: false,
});
self.select_previous(action, cx);
}
}
}
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) { pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx); let mut selections = this.selections.all::<Point>(cx);
@ -5586,6 +5735,7 @@ impl Editor {
if let Some(entry) = self.selection_history.undo_stack.pop_back() { if let Some(entry) = self.selection_history.undo_stack.pop_back() {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state; self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state; self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx); self.request_autoscroll(Autoscroll::newest(), cx);
} }
@ -5598,6 +5748,7 @@ impl Editor {
if let Some(entry) = self.selection_history.redo_stack.pop_back() { if let Some(entry) = self.selection_history.redo_stack.pop_back() {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec())); self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state; self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state; self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx); self.request_autoscroll(Autoscroll::newest(), cx);
} }
@ -6375,6 +6526,7 @@ impl Editor {
self.selection_history.push(SelectionHistoryEntry { self.selection_history.push(SelectionHistoryEntry {
selections: self.selections.disjoint_anchors(), selections: self.selections.disjoint_anchors(),
select_next_state: self.select_next_state.clone(), select_next_state: self.select_next_state.clone(),
select_prev_state: self.select_prev_state.clone(),
add_selections_state: self.add_selections_state.clone(), add_selections_state: self.add_selections_state.clone(),
}); });
} }
@ -6654,6 +6806,25 @@ impl Editor {
cx.notify(); cx.notify();
} }
pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
self.show_gutter = show_gutter;
cx.notify();
}
pub fn set_render_excerpt_header(
&mut self,
render_excerpt_header: impl 'static
+ Fn(
&mut Editor,
RenderExcerptHeaderParams,
&mut LayoutContext<Editor>,
) -> AnyElement<Editor>,
cx: &mut ViewContext<Self>,
) {
self.render_excerpt_header = Some(Arc::new(render_excerpt_header));
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) { pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@ -6878,7 +7049,7 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => { multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
} }
multi_buffer::Event::LanguageChanged => {} _ => {}
} }
} }
@ -7076,11 +7247,13 @@ impl Editor {
}; };
// If None, we are in a file without an extension // If None, we are in a file without an extension
let file_extension = file_extension.or(self let file = self
.buffer .buffer
.read(cx) .read(cx)
.as_singleton() .as_singleton()
.and_then(|b| b.read(cx).file()) .and_then(|b| b.read(cx).file());
let file_extension = file_extension.or(file
.as_ref()
.and_then(|file| Path::new(file.file_name(cx)).extension()) .and_then(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.map(|a| a.to_string())); .map(|a| a.to_string()));
@ -7091,7 +7264,7 @@ impl Editor {
.get("vim_mode") .get("vim_mode")
== Some(&serde_json::Value::Bool(true)); == Some(&serde_json::Value::Bool(true));
let telemetry_settings = *settings::get::<TelemetrySettings>(cx); let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None); let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
let copilot_enabled_for_language = self let copilot_enabled_for_language = self
.buffer .buffer
.read(cx) .read(cx)
@ -7099,15 +7272,6 @@ impl Editor {
.show_copilot_suggestions; .show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone(); let telemetry = project.read(cx).client().telemetry().clone();
telemetry.report_mixpanel_event(
match name {
"open" => "open editor",
"save" => "save editor",
_ => name,
},
json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }),
telemetry_settings,
);
let event = ClickhouseEvent::Editor { let event = ClickhouseEvent::Editor {
file_extension, file_extension,
vim_mode, vim_mode,
@ -7299,8 +7463,12 @@ impl View for Editor {
}); });
} }
let mut editor = EditorElement::new(style.clone());
if let Some(render_excerpt_header) = self.render_excerpt_header.clone() {
editor = editor.with_render_excerpt_header(render_excerpt_header);
}
Stack::new() Stack::new()
.with_child(EditorElement::new(style.clone())) .with_child(editor)
.with_child(ChildView::new(&self.mouse_context_menu, cx)) .with_child(ChildView::new(&self.mouse_context_menu, cx))
.into_any() .into_any()
} }
@ -7801,13 +7969,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
} }
pub fn highlight_diagnostic_message( pub fn highlight_diagnostic_message(
inital_highlights: Vec<usize>, initial_highlights: Vec<usize>,
message: &str, message: &str,
) -> (String, Vec<usize>) { ) -> (String, Vec<usize>) {
let mut message_without_backticks = String::new(); let mut message_without_backticks = String::new();
let mut prev_offset = 0; let mut prev_offset = 0;
let mut inside_block = false; let mut inside_block = false;
let mut highlights = inital_highlights; let mut highlights = initial_highlights;
for (match_ix, (offset, _)) in message for (match_ix, (offset, _)) in message
.match_indices('`') .match_indices('`')
.chain([(message.len(), "")]) .chain([(message.len(), "")])

View file

@ -9,7 +9,8 @@ use gpui::{
executor::Deterministic, executor::Deterministic,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
platform::{WindowBounds, WindowOptions}, platform::{WindowBounds, WindowOptions},
serde_json, TestAppContext, serde_json::{self, json},
TestAppContext,
}; };
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
// Ensure we don't panic when navigation data contains invalid anchors *and* points. // Ensure we don't panic when navigation data contains invalid anchors *and* points.
let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor; let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
invalid_anchor.text_anchor.buffer_id = Some(999); invalid_anchor.text_anchor.buffer_id = Some(999);
let invalid_point = Point::new(9999, 0); let invalid_point = Point::new(9999, 0);
editor.navigate( editor.navigate(
@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
cursor_anchor: invalid_anchor, cursor_anchor: invalid_anchor,
cursor_position: invalid_point, cursor_position: invalid_point,
scroll_anchor: ScrollAnchor { scroll_anchor: ScrollAnchor {
top_anchor: invalid_anchor, anchor: invalid_anchor,
offset: Default::default(), offset: Default::default(),
}, },
scroll_top_row: invalid_point.row, scroll_top_row: invalid_point.row,
@ -1718,6 +1719,33 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
"}); "});
} }
#[gpui::test]
async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("//".into()),
..LanguageConfig::default()
},
None,
));
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
// Fooˇ
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
// Foo
//ˇ
"});
}
#[gpui::test] #[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) { fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
@ -3107,6 +3135,57 @@ 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_previous(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
{
// `Select previous` without a selection (selects wordwise)
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
{
// `Select previous` with a selection
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
}
}
#[gpui::test] #[gpui::test]
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
@ -4270,7 +4349,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
); );
assert!(!cx.read(|cx| editor.is_dirty(cx))); assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server // Set rust language override and assert overridden tabsize is sent to language server
update_test_settings(cx, |settings| { update_test_settings(cx, |settings| {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
@ -4384,7 +4463,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
); );
assert!(!cx.read(|cx| editor.is_dirty(cx))); assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server // Set rust language override and assert overridden tabsize is sent to language server
update_test_settings(cx, |settings| { update_test_settings(cx, |settings| {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
@ -4725,7 +4804,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
two two
threeˇ threeˇ
"}, "},
"overlapping aditional edit", "overlapping additional edit",
), ),
( (
indoc! {" indoc! {"
@ -5225,7 +5304,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
Point::new(0, 1)..Point::new(0, 1), Point::new(0, 1)..Point::new(0, 1),
Point::new(1, 1)..Point::new(1, 1), Point::new(1, 1)..Point::new(1, 1),
] ]
) );
// Ensure the cursor's head is respected when deleting across an excerpt boundary.
view.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
});
view.backspace(&Default::default(), cx);
assert_eq!(view.text(cx), "Xa\nbbb");
assert_eq!(
view.selections.ranges(cx),
[Point::new(1, 0)..Point::new(1, 0)]
);
view.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
});
view.backspace(&Default::default(), cx);
assert_eq!(view.text(cx), "X\nbb");
assert_eq!(
view.selections.ranges(cx),
[Point::new(0, 1)..Point::new(0, 1)]
);
}); });
} }
@ -5742,7 +5842,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
follower.set_scroll_anchor( follower.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
top_anchor, anchor: top_anchor,
offset: vec2f(0.0, 0.5), offset: vec2f(0.0, 0.5),
}, },
cx, cx,

View file

@ -91,18 +91,41 @@ impl SelectionLayout {
} }
} }
#[derive(Clone)] pub struct RenderExcerptHeaderParams<'a> {
pub id: crate::ExcerptId,
pub buffer: &'a language::BufferSnapshot,
pub range: &'a crate::ExcerptRange<text::Anchor>,
pub starts_new_buffer: bool,
pub gutter_padding: f32,
pub editor_style: &'a EditorStyle,
}
pub type RenderExcerptHeader = Arc<
dyn Fn(
&mut Editor,
RenderExcerptHeaderParams,
&mut LayoutContext<Editor>,
) -> AnyElement<Editor>,
>;
pub struct EditorElement { pub struct EditorElement {
style: Arc<EditorStyle>, style: Arc<EditorStyle>,
render_excerpt_header: RenderExcerptHeader,
} }
impl EditorElement { impl EditorElement {
pub fn new(style: EditorStyle) -> Self { pub fn new(style: EditorStyle) -> Self {
Self { Self {
style: Arc::new(style), style: Arc::new(style),
render_excerpt_header: Arc::new(render_excerpt_header),
} }
} }
pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self {
self.render_excerpt_header = render;
self
}
fn attach_mouse_handlers( fn attach_mouse_handlers(
scene: &mut SceneBuilder, scene: &mut SceneBuilder,
position_map: &Arc<PositionMap>, position_map: &Arc<PositionMap>,
@ -1465,11 +1488,9 @@ impl EditorElement {
line_height: f32, line_height: f32,
style: &EditorStyle, style: &EditorStyle,
line_layouts: &[LineWithInvisibles], line_layouts: &[LineWithInvisibles],
include_root: bool,
editor: &mut Editor, editor: &mut Editor,
cx: &mut LayoutContext<Editor>, cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) { ) -> (f32, Vec<BlockLayout>) {
let tooltip_style = theme::current(cx).tooltip.clone();
let scroll_x = snapshot.scroll_anchor.offset.x(); let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone()) .blocks_in_range(rows.clone())
@ -1510,112 +1531,18 @@ impl EditorElement {
range, range,
starts_new_buffer, starts_new_buffer,
.. ..
} => { } => (self.render_excerpt_header)(
let id = *id; editor,
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { RenderExcerptHeaderParams {
let jump_path = ProjectPath { id: *id,
worktree_id: file.worktree_id(cx), buffer,
path: file.path.clone(), range,
}; starts_new_buffer: *starts_new_buffer,
let jump_anchor = range gutter_padding,
.primary editor_style: style,
.as_ref() },
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, editor, cx| {
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Editor::jump(
workspace,
jump_path.clone(),
jump_position,
jump_anchor,
cx, cx,
); ),
});
}
})
.with_tooltip::<JumpIcon>(
id.into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
cx,
)
.aligned()
.flex_float()
});
if *starts_new_buffer {
let style = &self.style.diagnostic_path_header;
let font_size =
(style.text_scale_factor * self.style.text.font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
let mut parent_path = None;
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path =
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
}
Flex::row()
.with_child(
Label::new(
filename.unwrap_or_else(|| "untitled".to_string()),
style.filename.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.filename.container)
.aligned(),
)
.with_children(parent_path.map(|path| {
Label::new(path, style.path.text.clone().with_font_size(font_size))
.contained()
.with_style(style.path.container)
.aligned()
}))
.with_children(jump_icon)
.contained()
.with_style(style.container)
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("path header block")
} else {
let text_style = self.style.text.clone();
Flex::row()
.with_child(Label::new("", text_style))
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("collapsed context")
}
}
}; };
element.layout( element.layout(
@ -1899,7 +1826,7 @@ impl Element<Editor> for EditorElement {
let gutter_padding; let gutter_padding;
let gutter_width; let gutter_width;
let gutter_margin; let gutter_margin;
if snapshot.mode == EditorMode::Full { if snapshot.show_gutter {
let em_width = style.text.em_width(cx.font_cache()); let em_width = style.text.em_width(cx.font_cache());
gutter_padding = (em_width * style.gutter_padding_factor).round(); gutter_padding = (em_width * style.gutter_padding_factor).round();
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
@ -2080,12 +2007,6 @@ impl Element<Editor> for EditorElement {
ShowScrollbar::Never => false, ShowScrollbar::Never => false,
}; };
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
.into_iter() .into_iter()
.map(|(id, fold)| { .map(|(id, fold)| {
@ -2144,7 +2065,6 @@ impl Element<Editor> for EditorElement {
line_height, line_height,
&style, &style,
&line_layouts, &line_layouts,
include_root,
editor, editor,
cx, cx,
); );
@ -2759,6 +2679,121 @@ impl HighlightedRange {
} }
} }
fn render_excerpt_header(
editor: &mut Editor,
RenderExcerptHeaderParams {
id,
buffer,
range,
starts_new_buffer,
gutter_padding,
editor_style,
}: RenderExcerptHeaderParams,
cx: &mut LayoutContext<Editor>,
) -> AnyElement<Editor> {
let tooltip_style = theme::current(cx).tooltip.clone();
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
};
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
let style = editor_style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, editor, cx| {
if let Some(workspace) = editor
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade(cx))
{
workspace.update(cx, |workspace, cx| {
Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx);
});
}
})
.with_tooltip::<JumpIcon>(
id.into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
cx,
)
.aligned()
.flex_float()
});
if starts_new_buffer {
let style = &editor_style.diagnostic_path_header;
let font_size = (style.text_scale_factor * editor_style.text.font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
let mut parent_path = None;
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/");
}
Flex::row()
.with_child(
Label::new(
filename.unwrap_or_else(|| "untitled".to_string()),
style.filename.text.clone().with_font_size(font_size),
)
.contained()
.with_style(style.filename.container)
.aligned(),
)
.with_children(parent_path.map(|path| {
Label::new(path, style.path.text.clone().with_font_size(font_size))
.contained()
.with_style(style.path.container)
.aligned()
}))
.with_children(jump_icon)
.contained()
.with_style(style.container)
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("path header block")
} else {
let text_style = editor_style.text.clone();
Flex::row()
.with_child(Label::new("", text_style))
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
.with_padding_right(gutter_padding)
.expanded()
.into_any_named("collapsed context")
}
}
fn position_to_display_point( fn position_to_display_point(
position: Vector2F, position: Vector2F,
text_bounds: RectF, text_bounds: RectF,
@ -3080,7 +3115,7 @@ mod tests {
editor_width: f32, editor_width: f32,
) -> Vec<Invisible> { ) -> Vec<Invisible> {
info!( info!(
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'" "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
); );
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&input_text, cx); let buffer = MultiBuffer::build_simple(&input_text, cx);

View file

@ -221,6 +221,7 @@ fn show_hover(
project: project.clone(), project: project.clone(),
symbol_range: range, symbol_range: range,
blocks: hover_result.contents, blocks: hover_result.contents,
language: hover_result.language,
rendered_content: None, rendered_content: None,
}) })
}); });
@ -253,6 +254,7 @@ fn render_blocks(
theme_id: usize, theme_id: usize,
blocks: &[HoverBlock], blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>, language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
style: &EditorStyle, style: &EditorStyle,
) -> RenderedInfo { ) -> RenderedInfo {
let mut text = String::new(); let mut text = String::new();
@ -351,11 +353,13 @@ fn render_blocks(
} }
Tag::CodeBlock(kind) => { Tag::CodeBlock(kind) => {
new_paragraph(&mut text, &mut list_stack); new_paragraph(&mut text, &mut list_stack);
if let CodeBlockKind::Fenced(language) = kind { current_language = if let CodeBlockKind::Fenced(language) = kind {
current_language = language_registry language_registry
.language_for_name(language.as_ref()) .language_for_name(language.as_ref())
.now_or_never() .now_or_never()
.and_then(Result::ok); .and_then(Result::ok)
} else {
language.cloned()
} }
} }
Tag::Emphasis => italic_depth += 1, Tag::Emphasis => italic_depth += 1,
@ -414,10 +418,6 @@ fn render_blocks(
} }
} }
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
RenderedInfo { RenderedInfo {
theme_id, theme_id,
text, text,
@ -524,6 +524,7 @@ pub struct InfoPopover {
pub project: ModelHandle<Project>, pub project: ModelHandle<Project>,
pub symbol_range: Range<Anchor>, pub symbol_range: Range<Anchor>,
pub blocks: Vec<HoverBlock>, pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RenderedInfo>, rendered_content: Option<RenderedInfo>,
} }
@ -559,6 +560,7 @@ impl InfoPopover {
style.theme_id, style.theme_id,
&self.blocks, &self.blocks,
self.project.read(cx).languages(), self.project.read(cx).languages(),
self.language.as_ref(),
style, style,
) )
}); });
@ -588,10 +590,7 @@ impl InfoPopover {
MouseRegion::new::<Self>(view_id, region_id, bounds) MouseRegion::new::<Self>(view_id, region_id, bounds)
.on_click::<Editor, _>( .on_click::<Editor, _>(
MouseButton::Left, MouseButton::Left,
move |_, _, cx| { move |_, _, cx| cx.platform().open_url(&url),
println!("clicked link {url}");
cx.platform().open_url(&url);
},
), ),
); );
} }
@ -906,7 +905,7 @@ mod tests {
text: "one **two** three".to_string(), text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
expected_marked_text: "one «two» three\n".to_string(), expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
weight: Some(Weight::BOLD), weight: Some(Weight::BOLD),
..Default::default() ..Default::default()
@ -918,7 +917,7 @@ mod tests {
text: "one [two](the-url) three".to_string(), text: "one [two](the-url) three".to_string(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
expected_marked_text: "one «two» three\n".to_string(), expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
underline: Some(Underline { underline: Some(Underline {
thickness: 1.0.into(), thickness: 1.0.into(),
@ -937,8 +936,7 @@ mod tests {
- b - b
* two * two
- [c](the-url) - [c](the-url)
- d - d"
"
.unindent(), .unindent(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
@ -949,8 +947,7 @@ mod tests {
- b - b
- two - two
- «c» - «c»
- d - d"
"
.unindent(), .unindent(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
underline: Some(Underline { underline: Some(Underline {
@ -973,8 +970,7 @@ mod tests {
nine nine
* ten * ten
* six * six"
"
.unindent(), .unindent(),
kind: HoverBlockKind::Markdown, kind: HoverBlockKind::Markdown,
}], }],
@ -985,8 +981,7 @@ mod tests {
nine nine
- ten - ten
- six - six"
"
.unindent(), .unindent(),
expected_styles: vec![HighlightStyle { expected_styles: vec![HighlightStyle {
underline: Some(Underline { underline: Some(Underline {
@ -1004,7 +999,7 @@ mod tests {
expected_styles, expected_styles,
} in &rows[0..] } in &rows[0..]
{ {
let rendered = render_blocks(0, &blocks, &Default::default(), &style); let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges let expected_highlights = ranges

View file

@ -196,7 +196,7 @@ impl FollowableItem for Editor {
singleton: buffer.is_singleton(), singleton: buffer.is_singleton(),
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
excerpts, excerpts,
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)), scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
scroll_x: scroll_anchor.offset.x(), scroll_x: scroll_anchor.offset.x(),
scroll_y: scroll_anchor.offset.y(), scroll_y: scroll_anchor.offset.y(),
selections: self selections: self
@ -253,7 +253,7 @@ impl FollowableItem for Editor {
} }
Event::ScrollPositionChanged { .. } => { Event::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor(); let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor)); update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
update.scroll_x = scroll_anchor.offset.x(); update.scroll_x = scroll_anchor.offset.x();
update.scroll_y = scroll_anchor.offset.y(); update.scroll_y = scroll_anchor.offset.y();
true true
@ -412,7 +412,7 @@ async fn update_editor_from_message(
} else if let Some(scroll_top_anchor) = scroll_top_anchor { } else if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote( editor.set_scroll_anchor_remote(
ScrollAnchor { ScrollAnchor {
top_anchor: scroll_top_anchor, anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y), offset: vec2f(message.scroll_x, message.scroll_y),
}, },
cx, cx,
@ -510,8 +510,8 @@ impl Item for Editor {
}; };
let mut scroll_anchor = data.scroll_anchor; let mut scroll_anchor = data.scroll_anchor;
if !buffer.can_resolve(&scroll_anchor.top_anchor) { if !buffer.can_resolve(&scroll_anchor.anchor) {
scroll_anchor.top_anchor = buffer.anchor_before( scroll_anchor.anchor = buffer.anchor_before(
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
); );
} }
@ -1231,6 +1231,10 @@ mod tests {
unimplemented!() unimplemented!()
} }
fn worktree_id(&self) -> usize {
0
}
fn is_deleted(&self) -> bool { fn is_deleted(&self) -> bool {
unimplemented!() unimplemented!()
} }

View file

@ -64,6 +64,9 @@ pub enum Event {
ExcerptsRemoved { ExcerptsRemoved {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
Edited, Edited,
Reloaded, Reloaded,
DiffBaseChanged, DiffBaseChanged,
@ -196,6 +199,13 @@ pub struct MultiBufferBytes<'a> {
chunk: &'a [u8], chunk: &'a [u8],
} }
pub struct ReversedMultiBufferBytes<'a> {
range: Range<usize>,
excerpts: Cursor<'a, Excerpt, usize>,
excerpt_bytes: Option<ExcerptBytes<'a>>,
chunk: &'a [u8],
}
struct ExcerptChunks<'a> { struct ExcerptChunks<'a> {
content_chunks: BufferChunks<'a>, content_chunks: BufferChunks<'a>,
footer_height: usize, footer_height: usize,
@ -387,6 +397,7 @@ impl MultiBuffer {
original_indent_column: u32, original_indent_column: u32,
} }
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default(); let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
let mut edited_excerpt_ids = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>(); let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() { for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into(); let new_text: Arc<str> = new_text.into();
@ -403,6 +414,7 @@ impl MultiBuffer {
.start .start
.to_offset(&start_excerpt.buffer) .to_offset(&start_excerpt.buffer)
+ start_overshoot; + start_overshoot;
edited_excerpt_ids.push(start_excerpt.id);
cursor.seek(&range.end, Bias::Right, &()); cursor.seek(&range.end, Bias::Right, &());
if cursor.item().is_none() && range.end == *cursor.start() { if cursor.item().is_none() && range.end == *cursor.start() {
@ -428,6 +440,7 @@ impl MultiBuffer {
original_indent_column, original_indent_column,
}); });
} else { } else {
edited_excerpt_ids.push(end_excerpt.id);
let start_excerpt_range = buffer_start let start_excerpt_range = buffer_start
..start_excerpt ..start_excerpt
.range .range
@ -474,6 +487,7 @@ impl MultiBuffer {
is_insertion: false, is_insertion: false,
original_indent_column, original_indent_column,
}); });
edited_excerpt_ids.push(excerpt.id);
cursor.next(&()); cursor.next(&());
} }
} }
@ -546,6 +560,10 @@ impl MultiBuffer {
buffer.edit(insertions, insertion_autoindent_mode, cx); buffer.edit(insertions, insertion_autoindent_mode, cx);
}) })
} }
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
} }
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> { pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
@ -1377,8 +1395,14 @@ impl MultiBuffer {
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
let language = self.language_at(point, cx); let mut language = None;
language_settings(language.map(|l| l.name()).as_deref(), cx) let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
let buffer = buffer.read(cx);
language = buffer.language_at(offset);
file = buffer.file();
}
language_settings(language.as_ref(), file, cx)
} }
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) { pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
@ -1961,7 +1985,6 @@ impl MultiBufferSnapshot {
} else { } else {
None None
}; };
MultiBufferBytes { MultiBufferBytes {
range, range,
excerpts, excerpts,
@ -1970,6 +1993,33 @@ impl MultiBufferSnapshot {
} }
} }
pub fn reversed_bytes_in_range<T: ToOffset>(
&self,
range: Range<T>,
) -> ReversedMultiBufferBytes {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpts = self.excerpts.cursor::<usize>();
excerpts.seek(&range.end, Bias::Left, &());
let mut chunk = &[][..];
let excerpt_bytes = if let Some(excerpt) = excerpts.item() {
let mut excerpt_bytes = excerpt.reversed_bytes_in_range(
range.start - excerpts.start()..range.end - excerpts.start(),
);
chunk = excerpt_bytes.next().unwrap_or(&[][..]);
Some(excerpt_bytes)
} else {
None
};
ReversedMultiBufferBytes {
range,
excerpts,
excerpt_bytes,
chunk,
}
}
pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows {
let mut result = MultiBufferRows { let mut result = MultiBufferRows {
buffer_row_range: 0..0, buffer_row_range: 0..0,
@ -2785,9 +2835,13 @@ impl MultiBufferSnapshot {
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
self.point_to_buffer_offset(point) let mut language = None;
.map(|(buffer, offset)| buffer.settings_at(offset, cx)) let mut file = None;
.unwrap_or_else(|| language_settings(None, cx)) if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
language = buffer.language_at(offset);
file = buffer.file();
}
language_settings(language, file, cx)
} }
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> { pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
@ -3399,6 +3453,26 @@ impl Excerpt {
} }
} }
fn reversed_bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
let content_start = self.range.context.start.to_offset(&self.buffer);
let bytes_start = content_start + range.start;
let bytes_end = content_start + cmp::min(range.end, self.text_summary.len);
let footer_height = if self.has_trailing_newline
&& range.start <= self.text_summary.len
&& range.end > self.text_summary.len
{
1
} else {
0
};
let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end);
ExcerptBytes {
content_bytes,
footer_height,
}
}
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
if text_anchor if text_anchor
.cmp(&self.range.context.start, &self.buffer) .cmp(&self.range.context.start, &self.buffer)
@ -3717,6 +3791,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> {
} }
} }
impl<'a> ReversedMultiBufferBytes<'a> {
fn consume(&mut self, len: usize) {
self.range.end -= len;
self.chunk = &self.chunk[..self.chunk.len() - len];
if !self.range.is_empty() && self.chunk.is_empty() {
if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) {
self.chunk = chunk;
} else {
self.excerpts.next(&());
if let Some(excerpt) = self.excerpts.item() {
let mut excerpt_bytes =
excerpt.bytes_in_range(0..self.range.end - self.excerpts.start());
self.chunk = excerpt_bytes.next().unwrap();
self.excerpt_bytes = Some(excerpt_bytes);
}
}
}
}
}
impl<'a> io::Read for ReversedMultiBufferBytes<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let len = cmp::min(buf.len(), self.chunk.len());
buf[..len].copy_from_slice(&self.chunk[..len]);
buf[..len].reverse();
if len > 0 {
self.consume(len);
}
Ok(len)
}
}
impl<'a> Iterator for ExcerptBytes<'a> { impl<'a> Iterator for ExcerptBytes<'a> {
type Item = &'a [u8]; type Item = &'a [u8];
@ -5237,7 +5343,7 @@ mod tests {
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
// An undo in the multibuffer undoes the multibuffer transaction // An undo in the multibuffer undoes the multibuffer transaction
// and also any individual buffer edits that have occured since // and also any individual buffer edits that have occurred since
// that transaction. // that transaction.
multibuffer.undo(cx); multibuffer.undo(cx);
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");

View file

@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool);
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor { pub struct ScrollAnchor {
pub offset: Vector2F, pub offset: Vector2F,
pub top_anchor: Anchor, pub anchor: Anchor,
} }
impl ScrollAnchor { impl ScrollAnchor {
fn new() -> Self { fn new() -> Self {
Self { Self {
offset: Vector2F::zero(), offset: Vector2F::zero(),
top_anchor: Anchor::min(), anchor: Anchor::min(),
} }
} }
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F { pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
let mut scroll_position = self.offset; let mut scroll_position = self.offset;
if self.top_anchor != Anchor::min() { if self.anchor != Anchor::min() {
let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32; let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
scroll_position.set_y(scroll_top + scroll_position.y()); scroll_position.set_y(scroll_top + scroll_position.y());
} else { } else {
scroll_position.set_y(0.); scroll_position.set_y(0.);
@ -59,7 +59,7 @@ impl ScrollAnchor {
} }
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
self.top_anchor.to_point(buffer).row self.anchor.to_point(buffer).row
} }
} }
@ -179,7 +179,7 @@ impl ScrollManager {
let (new_anchor, top_row) = if scroll_position.y() <= 0. { let (new_anchor, top_row) = if scroll_position.y() <= 0. {
( (
ScrollAnchor { ScrollAnchor {
top_anchor: Anchor::min(), anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)), offset: scroll_position.max(vec2f(0., 0.)),
}, },
0, 0,
@ -193,7 +193,7 @@ impl ScrollManager {
( (
ScrollAnchor { ScrollAnchor {
top_anchor, anchor: top_anchor,
offset: vec2f( offset: vec2f(
scroll_position.x(), scroll_position.x(),
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32, scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
@ -322,7 +322,7 @@ impl Editor {
hide_hover(self, cx); hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor let top_row = scroll_anchor
.top_anchor .anchor
.to_point(&self.buffer().read(cx).snapshot(cx)) .to_point(&self.buffer().read(cx).snapshot(cx))
.row; .row;
self.scroll_manager self.scroll_manager
@ -337,7 +337,7 @@ impl Editor {
hide_hover(self, cx); hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor let top_row = scroll_anchor
.top_anchor .anchor
.to_point(&self.buffer().read(cx).snapshot(cx)) .to_point(&self.buffer().read(cx).snapshot(cx))
.row; .row;
self.scroll_manager self.scroll_manager
@ -377,7 +377,7 @@ impl Editor {
let screen_top = self let screen_top = self
.scroll_manager .scroll_manager
.anchor .anchor
.top_anchor .anchor
.to_display_point(&snapshot); .to_display_point(&snapshot);
if screen_top > newest_head { if screen_top > newest_head {
@ -408,7 +408,7 @@ impl Editor {
.anchor_at(Point::new(top_row as u32, 0), Bias::Left); .anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor { let scroll_anchor = ScrollAnchor {
offset: Vector2F::new(x, y), offset: Vector2F::new(x, y),
top_anchor, anchor: top_anchor,
}; };
self.set_scroll_anchor(scroll_anchor, cx); self.set_scroll_anchor(scroll_anchor, cx);
} }

View file

@ -86,7 +86,7 @@ impl Editor {
editor.set_scroll_anchor( editor.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
top_anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),
}, },
cx, cx,
@ -113,7 +113,7 @@ impl Editor {
editor.set_scroll_anchor( editor.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
top_anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),
}, },
cx, cx,
@ -143,7 +143,7 @@ impl Editor {
editor.set_scroll_anchor( editor.set_scroll_anchor(
ScrollAnchor { ScrollAnchor {
top_anchor: new_anchor, anchor: new_anchor,
offset: Default::default(), offset: Default::default(),
}, },
cx, cx,

View file

@ -76,6 +76,9 @@ impl SelectionsCollection {
count count
} }
/// The non-pending, non-overlapping selections. There could still be a pending
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
/// selections over Anchors.
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> { pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
self.disjoint.clone() self.disjoint.clone()
} }

View file

@ -48,8 +48,8 @@ pub fn marked_display_snapshot(
} }
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) { pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true); let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), umarked_text); assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
} }

View file

@ -6,6 +6,8 @@ use std::{env, fmt::Display};
use sysinfo::{System, SystemExt}; use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel; use util::channel::ReleaseChannel;
// TODO: Move this file out of feedback and into a more general place
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs { pub struct SystemSpecs {
#[serde(serialize_with = "serialize_app_version")] #[serde(serialize_with = "serialize_app_version")]

View file

@ -33,7 +33,7 @@ use repository::{FakeGitRepositoryState, GitFileStatus};
use std::sync::Weak; use std::sync::Weak;
lazy_static! { lazy_static! {
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
} }
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@ -78,13 +78,13 @@ impl LineEnding {
} }
pub fn normalize(text: &mut String) { pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") { if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
*text = replaced; *text = replaced;
} }
} }
pub fn normalize_arc(text: Arc<str>) -> Arc<str> { pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") { if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
replaced.into() replaced.into()
} else { } else {
text text

View file

@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0" waker-fn = "1.1.0"
[build-dependencies] [build-dependencies]
bindgen = "0.59.2" bindgen = "0.65.1"
cc = "1.0.67" cc = "1.0.67"
[dev-dependencies] [dev-dependencies]

View file

@ -6335,9 +6335,9 @@ mod tests {
#[crate::test(self)] #[crate::test(self)]
async fn test_labeled_tasks(cx: &mut TestAppContext) { async fn test_labeled_tasks(cx: &mut TestAppContext) {
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next())); assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
let (mut sender, mut reciever) = postage::oneshot::channel::<()>(); let (mut sender, mut receiver) = postage::oneshot::channel::<()>();
let task = cx let task = cx
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await })); .update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await }));
assert_eq!( assert_eq!(
Some("Test Label"), Some("Test Label"),

View file

@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> {
} }
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> { pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
let root_view_id = self.window.root_view().id(); let focused_view_id = self.window.focused_view_id?;
self.window self.window
.rendered_views .rendered_views
.get(&root_view_id)? .get(&focused_view_id)?
.rect_for_text_range(range_utf16, self) .rect_for_text_range(range_utf16, self)
.log_err() .log_err()
.flatten() .flatten()

View file

@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler {
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> { fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
self.app self.app
.borrow_mut() .borrow()
.update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
.flatten() .flatten()
} }
} }

View file

@ -36,7 +36,7 @@ struct StateInner {
scroll_to: Option<ScrollTarget>, scroll_to: Option<ScrollTarget>,
} }
pub struct LayoutState<V: View> { pub struct UniformListLayoutState<V: View> {
scroll_max: f32, scroll_max: f32,
item_height: f32, item_height: f32,
items: Vec<AnyElement<V>>, items: Vec<AnyElement<V>>,
@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
} }
impl<V: View> Element<V> for UniformList<V> { impl<V: View> Element<V> for UniformList<V> {
type LayoutState = LayoutState<V>; type LayoutState = UniformListLayoutState<V>;
type PaintState = (); type PaintState = ();
fn layout( fn layout(
@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
let no_items = ( let no_items = (
constraint.min, constraint.min,
LayoutState { UniformListLayoutState {
item_height: 0., item_height: 0.,
scroll_max: 0., scroll_max: 0.,
items: Default::default(), items: Default::default(),
@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
( (
size, size,
LayoutState { UniformListLayoutState {
item_height, item_height,
scroll_max, scroll_max,
items, items,

View file

@ -67,7 +67,7 @@ impl KeymapMatcher {
/// MatchResult::Pending => /// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys. /// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) => /// MatchResult::Complete(matches) =>
/// 1 or more bindings have recieved the necessary key presses. /// 1 or more bindings have received the necessary key presses.
/// The order of the matched actions is by position of the matching first, /// The order of the matched actions is by position of the matching first,
// and order in the keymap second. // and order in the keymap second.
pub fn push_keystroke( pub fn push_keystroke(

View file

@ -216,6 +216,11 @@ pub trait File: Send + Sync {
/// of its worktree, then this method will return the name of the worktree itself. /// of its worktree, then this method will return the name of the worktree itself.
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr; fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
/// Returns the id of the worktree to which this file belongs.
///
/// This is needed for looking up project-specific settings.
fn worktree_id(&self) -> usize;
fn is_deleted(&self) -> bool; fn is_deleted(&self) -> bool;
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
@ -1802,8 +1807,7 @@ impl BufferSnapshot {
} }
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize { pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let language_name = self.language_at(position).map(|language| language.name()); let settings = language_settings(self.language_at(position), self.file(), cx);
let settings = language_settings(language_name.as_deref(), cx);
if settings.hard_tabs { if settings.hard_tabs {
IndentSize::tab() IndentSize::tab()
} else { } else {
@ -2127,8 +2131,7 @@ impl BufferSnapshot {
position: D, position: D,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> &'a LanguageSettings {
let language = self.language_at(position); language_settings(self.language_at(position), self.file.as_ref(), cx)
language_settings(language.map(|l| l.name()).as_deref(), cx)
} }
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> { pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
@ -2250,7 +2253,7 @@ impl BufferSnapshot {
} }
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> { pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
self.outline_items_containing(0..self.len(), theme) self.outline_items_containing(0..self.len(), true, theme)
.map(Outline::new) .map(Outline::new)
} }
@ -2262,6 +2265,7 @@ impl BufferSnapshot {
let position = position.to_offset(self); let position = position.to_offset(self);
let mut items = self.outline_items_containing( let mut items = self.outline_items_containing(
position.saturating_sub(1)..self.len().min(position + 1), position.saturating_sub(1)..self.len().min(position + 1),
false,
theme, theme,
)?; )?;
let mut prev_depth = None; let mut prev_depth = None;
@ -2276,6 +2280,7 @@ impl BufferSnapshot {
fn outline_items_containing( fn outline_items_containing(
&self, &self,
range: Range<usize>, range: Range<usize>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>, theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> { ) -> Option<Vec<OutlineItem<Anchor>>> {
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
@ -2310,7 +2315,10 @@ impl BufferSnapshot {
let node_is_name; let node_is_name;
if capture.index == config.name_capture_ix { if capture.index == config.name_capture_ix {
node_is_name = true; node_is_name = true;
} else if Some(capture.index) == config.context_capture_ix { } else if Some(capture.index) == config.context_capture_ix
|| (Some(capture.index) == config.extra_context_capture_ix
&& include_extra_context)
{
node_is_name = false; node_is_name = false;
} else { } else {
continue; continue;
@ -2337,10 +2345,12 @@ impl BufferSnapshot {
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end, buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true, true,
); );
let mut last_buffer_range_end = 0;
for (buffer_range, is_name) in buffer_ranges { for (buffer_range, is_name) in buffer_ranges {
if !text.is_empty() { if !text.is_empty() && buffer_range.start > last_buffer_range_end {
text.push(' '); text.push(' ');
} }
last_buffer_range_end = buffer_range.end;
if is_name { if is_name {
let mut start = text.len(); let mut start = text.len();
let end = start + buffer_range.len(); let end = start + buffer_range.len();

View file

@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
); );
} }
#[gpui::test]
async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
let language = javascript_lang()
.with_outline_query(
r#"
(function_declaration
"function" @context
name: (_) @name
parameters: (formal_parameters
"(" @context.extra
")" @context.extra)) @item
"#,
)
.unwrap();
let text = r#"
function a() {}
function b(c) {}
"#
.unindent();
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
// extra context nodes are included in the outline.
let outline = snapshot.outline(None).unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a()", 0), ("function b( )", 0),]
);
// extra context nodes do not appear in breadcrumbs.
let symbols = snapshot.symbols_containing(3, None).unwrap();
assert_eq!(
symbols
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a", 0)]
);
}
#[gpui::test] #[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#" let text = r#"

View file

@ -34,7 +34,7 @@ use std::{
fmt::Debug, fmt::Debug,
hash::Hash, hash::Hash,
mem, mem,
ops::Range, ops::{Not, Range},
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str,
sync::{ sync::{
@ -455,6 +455,7 @@ struct OutlineConfig {
item_capture_ix: u32, item_capture_ix: u32,
name_capture_ix: u32, name_capture_ix: u32,
context_capture_ix: Option<u32>, context_capture_ix: Option<u32>,
extra_context_capture_ix: Option<u32>,
} }
struct InjectionConfig { struct InjectionConfig {
@ -500,6 +501,7 @@ struct AvailableLanguage {
grammar: tree_sitter::Language, grammar: tree_sitter::Language,
lsp_adapters: Vec<Arc<dyn LspAdapter>>, lsp_adapters: Vec<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries, get_queries: fn(&str) -> LanguageQueries,
loaded: bool,
} }
pub struct LanguageRegistry { pub struct LanguageRegistry {
@ -527,6 +529,7 @@ struct LanguageRegistryState {
subscription: (watch::Sender<()>, watch::Receiver<()>), subscription: (watch::Sender<()>, watch::Receiver<()>),
theme: Option<Arc<Theme>>, theme: Option<Arc<Theme>>,
version: usize, version: usize,
reload_count: usize,
} }
pub struct PendingLanguageServer { pub struct PendingLanguageServer {
@ -547,6 +550,7 @@ impl LanguageRegistry {
subscription: watch::channel(), subscription: watch::channel(),
theme: Default::default(), theme: Default::default(),
version: 0, version: 0,
reload_count: 0,
}), }),
language_server_download_dir: None, language_server_download_dir: None,
lsp_binary_statuses_tx, lsp_binary_statuses_tx,
@ -566,6 +570,14 @@ impl LanguageRegistry {
self.executor = Some(executor); self.executor = Some(executor);
} }
/// Clear out all of the loaded languages and reload them from scratch.
///
/// This is useful in development, when queries have changed.
#[cfg(debug_assertions)]
pub fn reload(&self) {
self.state.write().reload();
}
pub fn register( pub fn register(
&self, &self,
path: &'static str, path: &'static str,
@ -582,6 +594,7 @@ impl LanguageRegistry {
grammar, grammar,
lsp_adapters, lsp_adapters,
get_queries, get_queries,
loaded: false,
}); });
} }
@ -590,7 +603,7 @@ impl LanguageRegistry {
let mut result = state let mut result = state
.available_languages .available_languages
.iter() .iter()
.map(|l| l.config.name.to_string()) .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
.chain(state.languages.iter().map(|l| l.config.name.to_string())) .chain(state.languages.iter().map(|l| l.config.name.to_string()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
@ -603,6 +616,7 @@ impl LanguageRegistry {
state state
.available_languages .available_languages
.iter() .iter()
.filter(|l| !l.loaded)
.flat_map(|l| l.lsp_adapters.clone()) .flat_map(|l| l.lsp_adapters.clone())
.chain( .chain(
state state
@ -639,10 +653,17 @@ impl LanguageRegistry {
self.state.read().subscription.1.clone() self.state.read().subscription.1.clone()
} }
/// The number of times that the registry has been changed,
/// by adding languages or reloading.
pub fn version(&self) -> usize { pub fn version(&self) -> usize {
self.state.read().version self.state.read().version
} }
/// The number of times that the registry has been reloaded.
pub fn reload_count(&self) -> usize {
self.state.read().reload_count
}
pub fn set_theme(&self, theme: Arc<Theme>) { pub fn set_theme(&self, theme: Arc<Theme>) {
let mut state = self.state.write(); let mut state = self.state.write();
state.theme = Some(theme.clone()); state.theme = Some(theme.clone());
@ -721,7 +742,7 @@ impl LanguageRegistry {
if let Some(language) = state if let Some(language) = state
.available_languages .available_languages
.iter() .iter()
.find(|l| callback(&l.config)) .find(|l| !l.loaded && callback(&l.config))
.cloned() .cloned()
{ {
let txs = state let txs = state
@ -743,9 +764,7 @@ impl LanguageRegistry {
let language = Arc::new(language); let language = Arc::new(language);
let mut state = this.state.write(); let mut state = this.state.write();
state.add(language.clone()); state.add(language.clone());
state state.mark_language_loaded(id);
.available_languages
.retain(|language| language.id != id);
if let Some(mut txs) = state.loading_languages.remove(&id) { if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) { for tx in txs.drain(..) {
let _ = tx.send(Ok(language.clone())); let _ = tx.send(Ok(language.clone()));
@ -753,10 +772,9 @@ impl LanguageRegistry {
} }
} }
Err(err) => { Err(err) => {
log::error!("failed to load language {name} - {err}");
let mut state = this.state.write(); let mut state = this.state.write();
state state.mark_language_loaded(id);
.available_languages
.retain(|language| language.id != id);
if let Some(mut txs) = state.loading_languages.remove(&id) { if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) { for tx in txs.drain(..) {
let _ = tx.send(Err(anyhow!( let _ = tx.send(Err(anyhow!(
@ -905,6 +923,28 @@ impl LanguageRegistryState {
self.version += 1; self.version += 1;
*self.subscription.0.borrow_mut() = (); *self.subscription.0.borrow_mut() = ();
} }
#[cfg(debug_assertions)]
fn reload(&mut self) {
self.languages.clear();
self.version += 1;
self.reload_count += 1;
for language in &mut self.available_languages {
language.loaded = false;
}
*self.subscription.0.borrow_mut() = ();
}
/// Mark the given language a having been loaded, so that the
/// language registry won't try to load it again.
fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
for language in &mut self.available_languages {
if language.id == id {
language.loaded = true;
break;
}
}
}
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -1021,34 +1061,22 @@ impl Language {
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> { pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights { if let Some(query) = queries.highlights {
self = self self = self.with_highlights_query(query.as_ref())?;
.with_highlights_query(query.as_ref())
.expect("failed to evaluate highlights query");
} }
if let Some(query) = queries.brackets { if let Some(query) = queries.brackets {
self = self self = self.with_brackets_query(query.as_ref())?;
.with_brackets_query(query.as_ref())
.expect("failed to load brackets query");
} }
if let Some(query) = queries.indents { if let Some(query) = queries.indents {
self = self self = self.with_indents_query(query.as_ref())?;
.with_indents_query(query.as_ref())
.expect("failed to load indents query");
} }
if let Some(query) = queries.outline { if let Some(query) = queries.outline {
self = self self = self.with_outline_query(query.as_ref())?;
.with_outline_query(query.as_ref())
.expect("failed to load outline query");
} }
if let Some(query) = queries.injections { if let Some(query) = queries.injections {
self = self self = self.with_injection_query(query.as_ref())?;
.with_injection_query(query.as_ref())
.expect("failed to load injection query");
} }
if let Some(query) = queries.overrides { if let Some(query) = queries.overrides {
self = self self = self.with_override_query(query.as_ref())?;
.with_override_query(query.as_ref())
.expect("failed to load override query");
} }
Ok(self) Ok(self)
} }
@ -1064,12 +1092,14 @@ impl Language {
let mut item_capture_ix = None; let mut item_capture_ix = None;
let mut name_capture_ix = None; let mut name_capture_ix = None;
let mut context_capture_ix = None; let mut context_capture_ix = None;
let mut extra_context_capture_ix = None;
get_capture_indices( get_capture_indices(
&query, &query,
&mut [ &mut [
("item", &mut item_capture_ix), ("item", &mut item_capture_ix),
("name", &mut name_capture_ix), ("name", &mut name_capture_ix),
("context", &mut context_capture_ix), ("context", &mut context_capture_ix),
("context.extra", &mut extra_context_capture_ix),
], ],
); );
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@ -1078,6 +1108,7 @@ impl Language {
item_capture_ix, item_capture_ix,
name_capture_ix, name_capture_ix,
context_capture_ix, context_capture_ix,
extra_context_capture_ix,
}); });
} }
Ok(self) Ok(self)

View file

@ -1,3 +1,4 @@
use crate::{File, Language};
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
use globset::GlobMatcher; use globset::GlobMatcher;
@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
settings::register::<AllLanguageSettings>(cx); settings::register::<AllLanguageSettings>(cx);
} }
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings { pub fn language_settings<'a>(
settings::get::<AllLanguageSettings>(cx).language(language) language: Option<&Arc<Language>>,
file: Option<&Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language_name = language.map(|l| l.name());
all_language_settings(file, cx).language(language_name.as_deref())
} }
pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings { pub fn all_language_settings<'a>(
settings::get::<AllLanguageSettings>(cx) file: Option<&Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a AllLanguageSettings {
let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
settings::get_local(location, cx)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -155,7 +165,7 @@ impl AllLanguageSettings {
.any(|glob| glob.is_match(path)) .any(|glob| glob.is_match(path))
} }
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
if !self.copilot.feature_enabled { if !self.copilot.feature_enabled {
return false; return false;
} }
@ -166,7 +176,8 @@ impl AllLanguageSettings {
} }
} }
self.language(language_name).show_copilot_suggestions self.language(language.map(|l| l.name()).as_deref())
.show_copilot_suggestions
} }
} }
@ -253,7 +264,7 @@ impl settings::Setting for AllLanguageSettings {
let mut root_schema = generator.root_schema_for::<Self::FileContent>(); let mut root_schema = generator.root_schema_for::<Self::FileContent>();
// Create a schema for a 'languages overrides' object, associating editor // Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges. // settings with specific languages.
assert!(root_schema assert!(root_schema
.definitions .definitions
.contains_key("LanguageSettingsContent")); .contains_key("LanguageSettingsContent"));

View file

@ -773,7 +773,7 @@ impl<'a> SyntaxMapCaptures<'a> {
} in layers } in layers
{ {
let grammar = match &language.grammar { let grammar = match &language.grammar {
Some(grammer) => grammer, Some(grammar) => grammar,
None => continue, None => continue,
}; };
let query = match query(&grammar) { let query = match query(&grammar) {
@ -896,7 +896,7 @@ impl<'a> SyntaxMapMatches<'a> {
} in layers } in layers
{ {
let grammar = match &language.grammar { let grammar = match &language.grammar {
Some(grammer) => grammer, Some(grammar) => grammar,
None => continue, None => continue,
}; };
let query = match query(&grammar) { let query = match query(&grammar) {

View file

@ -260,9 +260,10 @@ impl LanguageServer {
buffer.clear(); buffer.clear();
stdout.read_until(b'\n', &mut buffer).await?; stdout.read_until(b'\n', &mut buffer).await?;
stdout.read_until(b'\n', &mut buffer).await?; stdout.read_until(b'\n', &mut buffer).await?;
let message_len: usize = std::str::from_utf8(&buffer)? let header = std::str::from_utf8(&buffer)?;
let message_len: usize = header
.strip_prefix(CONTENT_LEN_HEADER) .strip_prefix(CONTENT_LEN_HEADER)
.ok_or_else(|| anyhow!("invalid header"))? .ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
.trim_end() .trim_end()
.parse()?; .parse()?;
@ -301,7 +302,7 @@ impl LanguageServer {
} }
} else { } else {
warn!( warn!(
"Failed to deserialize message:\n{}", "failed to deserialize LSP message:\n{}",
std::str::from_utf8(&buffer)? std::str::from_utf8(&buffer)?
); );
} }

View file

@ -18,4 +18,4 @@ metal = "0.21.0"
objc = "0.2" objc = "0.2"
[build-dependencies] [build-dependencies]
bindgen = "0.59.2" bindgen = "0.65.1"

View file

@ -11,7 +11,7 @@ use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Ty
/// "Hello from Wasm".into() /// "Hello from Wasm".into()
/// } /// }
/// ``` /// ```
/// This macro makes a function defined guest-side avaliable host-side. /// This macro makes a function defined guest-side available host-side.
/// Note that all arguments and return types must be `serde`. /// Note that all arguments and return types must be `serde`.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn export(args: TokenStream, function: TokenStream) -> TokenStream { pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
/// #[import] /// #[import]
/// pub fn operating_system_name() -> String; /// pub fn operating_system_name() -> String;
/// ``` /// ```
/// This macro makes a function defined host-side avaliable guest-side. /// This macro makes a function defined host-side available guest-side.
/// Note that all arguments and return types must be `serde`. /// Note that all arguments and return types must be `serde`.
/// All that's provided is a signature, as the function is implemented host-side. /// All that's provided is a signature, as the function is implemented host-side.
#[proc_macro_attribute] #[proc_macro_attribute]

View file

@ -127,7 +127,7 @@ use plugin_handles::RopeHandle;
pub fn append(rope: RopeHandle, string: &str); pub fn append(rope: RopeHandle, string: &str);
``` ```
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle. This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle.
To illustrate that point, here's an example. First, we'd define a plugin-side function as follows: To illustrate that point, here's an example. First, we'd define a plugin-side function as follows:
@ -177,7 +177,7 @@ So here's what calling `append_newline` would do, from the top:
6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource. 6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource.
Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource. Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
## Final Notes ## Final Notes

View file

@ -132,7 +132,7 @@ impl PluginBuilder {
"env", "env",
&format!("__{}", name), &format!("__{}", name),
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
// TODO: use try block once avaliable // TODO: use try block once available
let result: Result<(WasiBuffer, Memory, _), Trap> = (|| { let result: Result<(WasiBuffer, Memory, _), Trap> = (|| {
// grab a handle to the memory // grab a handle to the memory
let plugin_memory = match caller.get_export("memory") { let plugin_memory = match caller.get_export("memory") {
@ -211,7 +211,7 @@ impl PluginBuilder {
"env", "env",
&format!("__{}", name), &format!("__{}", name),
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| { move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
// TODO: use try block once avaliable // TODO: use try block once available
let result: Result<(WasiBuffer, Memory, Vec<u8>), Trap> = (|| { let result: Result<(WasiBuffer, Memory, Vec<u8>), Trap> = (|| {
// grab a handle to the memory // grab a handle to the memory
let plugin_memory = match caller.get_export("memory") { let plugin_memory = match caller.get_export("memory") {
@ -297,7 +297,7 @@ pub enum PluginBinary<'a> {
Precompiled(&'a [u8]), Precompiled(&'a [u8]),
} }
/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface. /// Represents a WebAssembly plugin, with access to the WebAssembly System Interface.
/// Build a new plugin using [`PluginBuilder`]. /// Build a new plugin using [`PluginBuilder`].
pub struct Plugin { pub struct Plugin {
store: Store<WasiCtxAlloc>, store: Store<WasiCtxAlloc>,
@ -559,7 +559,7 @@ impl Plugin {
.ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?; .ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?;
// write the argument to linear memory // write the argument to linear memory
// this returns a (ptr, lentgh) pair // this returns a (ptr, length) pair
let arg_buffer = Self::bytes_to_buffer( let arg_buffer = Self::bytes_to_buffer(
self.store.data().alloc_buffer(), self.store.data().alloc_buffer(),
&mut plugin_memory, &mut plugin_memory,
@ -569,7 +569,7 @@ impl Plugin {
.await?; .await?;
// call the function, passing in the buffer and its length // call the function, passing in the buffer and its length
// this returns a ptr to a (ptr, lentgh) pair // this returns a ptr to a (ptr, length) pair
let result_buffer = handle let result_buffer = handle
.function .function
.call_async(&mut self.store, arg_buffer.into_u64()) .call_async(&mut self.store, arg_buffer.into_u64())

View file

@ -1111,14 +1111,18 @@ impl LspCommand for GetHover {
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self::Response> { ) -> Result<Self::Response> {
Ok(message.and_then(|hover| { Ok(message.and_then(|hover| {
let range = hover.range.map(|range| { let (language, range) = cx.read(|cx| {
cx.read(|cx| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
(
buffer.language().cloned(),
hover.range.map(|range| {
let token_start = let token_start =
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); let token_end =
buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
buffer.anchor_after(token_start)..buffer.anchor_before(token_end) buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
}) }),
)
}); });
fn hover_blocks_from_marked_string( fn hover_blocks_from_marked_string(
@ -1163,7 +1167,11 @@ impl LspCommand for GetHover {
}], }],
}); });
Some(Hover { contents, range }) Some(Hover {
contents,
range,
language,
})
})) }))
} }
@ -1247,16 +1255,9 @@ impl LspCommand for GetHover {
self, self,
message: proto::GetHoverResponse, message: proto::GetHoverResponse,
_: ModelHandle<Project>, _: ModelHandle<Project>,
_: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self::Response> { ) -> Result<Self::Response> {
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
language::proto::deserialize_anchor(start)
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
} else {
None
};
let contents: Vec<_> = message let contents: Vec<_> = message
.contents .contents
.into_iter() .into_iter()
@ -1271,12 +1272,23 @@ impl LspCommand for GetHover {
}, },
}) })
.collect(); .collect();
if contents.is_empty() {
return Ok(None);
}
Ok(if contents.is_empty() { let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
None let range = if let (Some(start), Some(end)) = (message.start, message.end) {
language::proto::deserialize_anchor(start)
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
} else { } else {
Some(Hover { contents, range }) None
}) };
Ok(Some(Hover {
contents,
range,
language,
}))
} }
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 { fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
@ -1499,7 +1511,11 @@ impl LspCommand for GetCodeActions {
type ProtoRequest = proto::GetCodeActions; type ProtoRequest = proto::GetCodeActions;
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool { fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
capabilities.code_action_provider.is_some() match &capabilities.code_action_provider {
None => false,
Some(lsp::CodeActionProviderCapability::Simple(false)) => false,
_ => true,
}
} }
fn to_lsp( fn to_lsp(
@ -1717,8 +1733,7 @@ impl LspCommand for OnTypeFormatting {
.await?; .await?;
let tab_size = buffer.read_with(&cx, |buffer, cx| { let tab_size = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); language_settings(buffer.language(), buffer.file(), cx).tab_size
language_settings(language_name.as_deref(), cx).tab_size
}); });
Ok(Self { Ok(Self {

View file

@ -28,7 +28,7 @@ use gpui::{
ModelHandle, Task, WeakModelHandle, ModelHandle, Task, WeakModelHandle,
}; };
use language::{ use language::{
language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, language_settings::{language_settings, FormatOnSave, Formatter},
point_to_lsp, point_to_lsp,
proto::{ proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@ -72,7 +72,10 @@ use std::{
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use terminals::Terminals; use terminals::Terminals;
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; use util::{
debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
ResultExt, TryFutureExt as _,
};
pub use fs::*; pub use fs::*;
pub use worktree::*; pub use worktree::*;
@ -356,6 +359,7 @@ pub enum HoverBlockKind {
pub struct Hover { pub struct Hover {
pub contents: Vec<HoverBlock>, pub contents: Vec<HoverBlock>,
pub range: Option<Range<language::Anchor>>, pub range: Option<Range<language::Anchor>>,
pub language: Option<Arc<Language>>,
} }
#[derive(Default)] #[derive(Default)]
@ -460,6 +464,7 @@ impl Project {
client.add_model_request_handler(Self::handle_update_buffer); client.add_model_request_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_diagnostic_summary);
client.add_model_message_handler(Self::handle_update_worktree); client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_message_handler(Self::handle_update_worktree_settings);
client.add_model_request_handler(Self::handle_create_project_entry); client.add_model_request_handler(Self::handle_create_project_entry);
client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_rename_project_entry);
client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry);
@ -519,7 +524,7 @@ impl Project {
_subscriptions: vec![ _subscriptions: vec![
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed) cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
], ],
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
active_entry: None, active_entry: None,
languages, languages,
@ -588,7 +593,7 @@ impl Project {
active_entry: None, active_entry: None,
collaborators: Default::default(), collaborators: Default::default(),
join_project_response_message_id: response.message_id, join_project_response_message_id: response.message_id,
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
languages, languages,
user_store: user_store.clone(), user_store: user_store.clone(),
@ -686,45 +691,40 @@ impl Project {
} }
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) { fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let settings = all_language_settings(cx);
let mut language_servers_to_start = Vec::new(); let mut language_servers_to_start = Vec::new();
for buffer in self.opened_buffers.values() { for buffer in self.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) { if let Some(buffer) = buffer.upgrade(cx) {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) if let Some((file, language)) = buffer.file().zip(buffer.language()) {
{ let settings = language_settings(Some(language), Some(file), cx);
if settings if settings.enable_language_server {
.language(Some(&language.name())) if let Some(file) = File::from_dyn(Some(file)) {
.enable_language_server language_servers_to_start
{ .push((file.worktree.clone(), language.clone()));
let worktree = file.worktree.read(cx); }
language_servers_to_start.push((
worktree.id(),
worktree.as_local().unwrap().abs_path().clone(),
language.clone(),
));
} }
} }
} }
} }
let mut language_servers_to_stop = Vec::new(); let mut language_servers_to_stop = Vec::new();
for language in self.languages.to_vec() { let languages = self.languages.to_vec();
for lsp_adapter in language.lsp_adapters() {
if !settings
.language(Some(&language.name()))
.enable_language_server
{
let lsp_name = &lsp_adapter.name;
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
if lsp_name == started_lsp_name { let language = languages.iter().find(|l| {
l.lsp_adapters()
.iter()
.any(|adapter| &adapter.name == started_lsp_name)
});
if let Some(language) = language {
let worktree = self.worktree_for_id(*worktree_id, cx);
let file = worktree.and_then(|tree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
});
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
} }
} }
} }
}
}
// Stop all newly-disabled language servers. // Stop all newly-disabled language servers.
for (worktree_id, adapter_name) in language_servers_to_stop { for (worktree_id, adapter_name) in language_servers_to_stop {
@ -733,8 +733,9 @@ impl Project {
} }
// Start all the newly-enabled language servers. // Start all the newly-enabled language servers.
for (worktree_id, worktree_path, language) in language_servers_to_start { for (worktree, language) in language_servers_to_start {
self.start_language_servers(worktree_id, worktree_path, language, cx); let worktree_path = worktree.read(cx).abs_path();
self.start_language_servers(&worktree, worktree_path, language, cx);
} }
if !self.copilot_enabled && Copilot::global(cx).is_some() { if !self.copilot_enabled && Copilot::global(cx).is_some() {
@ -1107,6 +1108,21 @@ impl Project {
.log_err(); .log_err();
} }
let store = cx.global::<SettingsStore>();
for worktree in self.worktrees(cx) {
let worktree_id = worktree.read(cx).id().to_proto();
for (path, content) in store.local_settings(worktree.id()) {
self.client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
})
.log_err();
}
}
let (updates_tx, mut updates_rx) = mpsc::unbounded(); let (updates_tx, mut updates_rx) = mpsc::unbounded();
let client = self.client.clone(); let client = self.client.clone();
self.client_state = Some(ProjectClientState::Local { self.client_state = Some(ProjectClientState::Local {
@ -1219,6 +1235,14 @@ impl Project {
message_id: u32, message_id: u32,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<()> { ) -> Result<()> {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
for worktree in &self.worktrees {
store
.clear_local_settings(worktree.handle_id(), cx)
.log_err();
}
});
self.join_project_response_message_id = message_id; self.join_project_response_message_id = message_id;
self.set_worktrees_from_proto(message.worktrees, cx)?; self.set_worktrees_from_proto(message.worktrees, cx)?;
self.set_collaborators_from_proto(message.collaborators, cx)?; self.set_collaborators_from_proto(message.collaborators, cx)?;
@ -2215,13 +2239,34 @@ impl Project {
} }
fn maintain_buffer_languages( fn maintain_buffer_languages(
languages: &LanguageRegistry, languages: Arc<LanguageRegistry>,
cx: &mut ModelContext<Project>, cx: &mut ModelContext<Project>,
) -> Task<()> { ) -> Task<()> {
let mut subscription = languages.subscribe(); let mut subscription = languages.subscribe();
let mut prev_reload_count = languages.reload_count();
cx.spawn_weak(|project, mut cx| async move { cx.spawn_weak(|project, mut cx| async move {
while let Some(()) = subscription.next().await { while let Some(()) = subscription.next().await {
if let Some(project) = project.upgrade(&cx) { if let Some(project) = project.upgrade(&cx) {
// If the language registry has been reloaded, then remove and
// re-assign the languages on all open buffers.
let reload_count = languages.reload_count();
if reload_count > prev_reload_count {
prev_reload_count = reload_count;
project.update(&mut cx, |this, cx| {
let buffers = this
.opened_buffers
.values()
.filter_map(|b| b.upgrade(cx))
.collect::<Vec<_>>();
for buffer in buffers {
if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() {
this.unregister_buffer_from_language_servers(&buffer, &f, cx);
buffer.update(cx, |buffer, cx| buffer.set_language(None, cx));
}
}
});
}
project.update(&mut cx, |project, cx| { project.update(&mut cx, |project, cx| {
let mut plain_text_buffers = Vec::new(); let mut plain_text_buffers = Vec::new();
let mut buffers_with_unknown_injections = Vec::new(); let mut buffers_with_unknown_injections = Vec::new();
@ -2321,25 +2366,34 @@ impl Project {
}); });
if let Some(file) = File::from_dyn(buffer.read(cx).file()) { if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
if let Some(worktree) = file.worktree.read(cx).as_local() { let worktree = file.worktree.clone();
let worktree_id = worktree.id(); if let Some(tree) = worktree.read(cx).as_local() {
let worktree_abs_path = worktree.abs_path().clone(); self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
} }
} }
} }
fn start_language_servers( fn start_language_servers(
&mut self, &mut self,
worktree_id: WorktreeId, worktree: &ModelHandle<Worktree>,
worktree_path: Arc<Path>, worktree_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if !language_settings(Some(&language.name()), cx).enable_language_server { if !language_settings(
Some(&language),
worktree
.update(cx, |tree, cx| tree.root_file(cx))
.map(|f| f as _)
.as_ref(),
cx,
)
.enable_language_server
{
return; return;
} }
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
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) {
@ -2748,23 +2802,22 @@ impl Project {
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>, buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<()> { ) -> Option<()> {
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = buffers
.into_iter() .into_iter()
.filter_map(|buffer| { .filter_map(|buffer| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let file = File::from_dyn(buffer.file())?; let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
let full_path = file.full_path(cx); let full_path = file.full_path(cx);
let language = self let language = self
.languages .languages
.language_for_file(&full_path, Some(buffer.as_rope())) .language_for_file(&full_path, Some(buffer.as_rope()))
.now_or_never()? .now_or_never()?
.ok()?; .ok()?;
Some((worktree.id(), worktree.abs_path().clone(), language)) Some((file.worktree.clone(), language))
}) })
.collect(); .collect();
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { for (worktree, language) in language_server_lookup_info {
self.restart_language_servers(worktree_id, worktree_abs_path, language, cx); self.restart_language_servers(worktree, language, cx);
} }
None None
@ -2773,11 +2826,13 @@ impl Project {
// TODO This will break in the case where the adapter's root paths and worktrees are not equal // 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_id: WorktreeId, worktree: ModelHandle<Worktree>,
fallback_path: Arc<Path>,
language: Arc<Language>, language: Arc<Language>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new(); let mut stops = Vec::new();
for adapter in language.lsp_adapters() { for adapter in language.lsp_adapters() {
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx)); stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
@ -2807,7 +2862,7 @@ impl Project {
.map(|path_buf| Arc::from(path_buf.as_path())) .map(|path_buf| Arc::from(path_buf.as_path()))
.unwrap_or(fallback_path); .unwrap_or(fallback_path);
this.start_language_servers(worktree_id, root_path, language.clone(), cx); 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 adapter in language.lsp_adapters() {
@ -3432,8 +3487,7 @@ impl Project {
let mut project_transaction = ProjectTransaction::default(); let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
let settings = buffer.read_with(&cx, |buffer, cx| { let settings = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); language_settings(buffer.language(), buffer.file(), cx).clone()
language_settings(language_name.as_deref(), cx).clone()
}); });
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@ -4020,7 +4074,7 @@ impl Project {
let end_within = range.start.cmp(&primary.end, buffer).is_le() let end_within = range.start.cmp(&primary.end, buffer).is_le()
&& range.end.cmp(&primary.end, buffer).is_ge(); && range.end.cmp(&primary.end, buffer).is_ge();
//Skip addtional edits which overlap with the primary completion edit //Skip additional edits which overlap with the primary completion edit
//https://github.com/zed-industries/zed/pull/1871 //https://github.com/zed-industries/zed/pull/1871
if !start_within && !end_within { if !start_within && !end_within {
buffer.edit([(range, text)], None, cx); buffer.edit([(range, text)], None, cx);
@ -4463,11 +4517,14 @@ impl Project {
push_to_history: bool, push_to_history: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> { ) -> Task<Result<Option<Transaction>>> {
let tab_size = buffer.read_with(cx, |buffer, cx| { let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name()); let position = position.to_point_utf16(buffer);
language_settings(language_name.as_deref(), cx).tab_size (
position,
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
.tab_size,
)
}); });
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp( self.request_lsp(
buffer.clone(), buffer.clone(),
OnTypeFormatting { OnTypeFormatting {
@ -4873,6 +4930,7 @@ impl Project {
worktree::Event::UpdatedEntries(changes) => { worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_buffers(&worktree, changes, cx); this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx); this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
} }
worktree::Event::UpdatedGitRepositories(updated_repos) => { worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx) this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
@ -4893,8 +4951,12 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade())); .push(WorktreeHandle::Weak(worktree.downgrade()));
} }
cx.observe_release(worktree, |this, worktree, cx| { let handle_id = worktree.id();
cx.observe_release(worktree, move |this, worktree, cx| {
let _ = this.remove_worktree(worktree.id(), cx); let _ = this.remove_worktree(worktree.id(), cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.clear_local_settings(handle_id, cx).log_err()
});
}) })
.detach(); .detach();
@ -5179,6 +5241,71 @@ impl Project {
.detach(); .detach();
} }
fn update_local_worktree_settings(
&mut self,
worktree: &ModelHandle<Worktree>,
changes: &UpdatedEntriesSet,
cx: &mut ModelContext<Self>,
) {
let project_id = self.remote_id();
let worktree_id = worktree.id();
let worktree = worktree.read(cx).as_local().unwrap();
let remote_worktree_id = worktree.id();
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
let settings_dir = Arc::from(
path.ancestors()
.nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
.unwrap(),
);
let fs = self.fs.clone();
let removed = *change == PathChange::Removed;
let abs_path = worktree.absolutize(path);
settings_contents.push(async move {
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
});
}
}
if settings_contents.is_empty() {
return;
}
let client = self.client.clone();
cx.spawn_weak(move |_, mut cx| async move {
let settings_contents: Vec<(Arc<Path>, _)> =
futures::future::join_all(settings_contents).await;
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
for (directory, file_content) in settings_contents {
let file_content = file_content.and_then(|content| content.log_err());
store
.set_local_settings(
worktree_id,
directory.clone(),
file_content.as_ref().map(String::as_str),
cx,
)
.log_err();
if let Some(remote_id) = project_id {
client
.send(proto::UpdateWorktreeSettings {
project_id: remote_id,
worktree_id: remote_worktree_id.to_proto(),
path: directory.to_string_lossy().into_owned(),
content: file_content,
})
.log_err();
}
}
});
});
})
.detach();
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) { pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| { let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@ -5431,6 +5558,30 @@ impl Project {
}) })
} }
async fn handle_update_worktree_settings(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store
.set_local_settings(
worktree.id(),
PathBuf::from(&envelope.payload.path).into(),
envelope.payload.content.as_ref().map(String::as_str),
cx,
)
.log_err();
});
}
Ok(())
})
}
async fn handle_create_project_entry( async fn handle_create_project_entry(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::CreateProjectEntry>, envelope: TypedEnvelope<proto::CreateProjectEntry>,
@ -6521,8 +6672,8 @@ impl Project {
} }
self.metadata_changed(cx); self.metadata_changed(cx);
for (id, _) in old_worktrees_by_id { for id in old_worktrees_by_id.keys() {
cx.emit(Event::WorktreeRemoved(id)); cx.emit(Event::WorktreeRemoved(*id));
} }
Ok(()) Ok(())
@ -6892,6 +7043,13 @@ impl WorktreeHandle {
WorktreeHandle::Weak(handle) => handle.upgrade(cx), WorktreeHandle::Weak(handle) => handle.upgrade(cx),
} }
} }
pub fn handle_id(&self) -> usize {
match self {
WorktreeHandle::Strong(handle) => handle.id(),
WorktreeHandle::Weak(handle) => handle.id(),
}
}
} }
impl OpenBuffer { impl OpenBuffer {

View file

@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_managing_project_specific_settings(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/the-root",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#
},
"a": {
"a.rs": "fn a() {\n A\n}"
},
"b": {
".zed": {
"settings.json": r#"{ "tab_size": 2 }"#
},
"b.rs": "fn b() {\n B\n}"
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
deterministic.run_until_parked();
cx.read(|cx| {
let tree = worktree.read(cx);
let settings_a = language_settings(
None,
Some(
&(File::for_entry(
tree.entry_for_path("a/a.rs").unwrap().clone(),
worktree.clone(),
) as _),
),
cx,
);
let settings_b = language_settings(
None,
Some(
&(File::for_entry(
tree.entry_for_path("b/b.rs").unwrap().clone(),
worktree.clone(),
) as _),
),
cx,
);
assert_eq!(settings_a.tab_size.get(), 8);
assert_eq!(settings_b.tab_size.get(), 2);
});
}
#[gpui::test] #[gpui::test]
async fn test_managing_language_servers( async fn test_managing_language_servers(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,

View file

@ -571,6 +571,11 @@ impl Worktree {
Worktree::Remote(worktree) => worktree.abs_path.clone(), Worktree::Remote(worktree) => worktree.abs_path.clone(),
} }
} }
pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<Arc<File>> {
let entry = self.root_entry()?;
Some(File::for_entry(entry.clone(), cx.handle()))
}
} }
impl LocalWorktree { impl LocalWorktree {
@ -578,14 +583,6 @@ impl LocalWorktree {
path.starts_with(&self.abs_path) path.starts_with(&self.abs_path)
} }
fn absolutize(&self, path: &Path) -> PathBuf {
if path.file_name().is_some() {
self.abs_path.join(path)
} else {
self.abs_path.to_path_buf()
}
}
pub(crate) fn load_buffer( pub(crate) fn load_buffer(
&mut self, &mut self,
id: u64, id: u64,
@ -1429,6 +1426,14 @@ impl Snapshot {
&self.abs_path &self.abs_path
} }
pub fn absolutize(&self, path: &Path) -> PathBuf {
if path.file_name().is_some() {
self.abs_path.join(path)
} else {
self.abs_path.to_path_buf()
}
}
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
self.entries_by_id.get(&entry_id, &()).is_some() self.entries_by_id.get(&entry_id, &()).is_some()
} }
@ -2360,6 +2365,10 @@ impl language::File for File {
.unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name)) .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
} }
fn worktree_id(&self) -> usize {
self.worktree.id()
}
fn is_deleted(&self) -> bool { fn is_deleted(&self) -> bool {
self.is_deleted self.is_deleted
} }
@ -2424,6 +2433,17 @@ impl language::LocalFile for File {
} }
impl File { impl File {
pub fn for_entry(entry: Entry, worktree: ModelHandle<Worktree>) -> Arc<Self> {
Arc::new(Self {
worktree,
path: entry.path.clone(),
mtime: entry.mtime,
entry_id: entry.id,
is_local: true,
is_deleted: false,
})
}
pub fn from_proto( pub fn from_proto(
proto: rpc::proto::File, proto: rpc::proto::File,
worktree: ModelHandle<Worktree>, worktree: ModelHandle<Worktree>,
@ -2485,7 +2505,7 @@ pub enum EntryKind {
File(CharBag), File(CharBag),
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathChange { pub enum PathChange {
/// A filesystem entry was was created. /// A filesystem entry was was created.
Added, Added,
@ -3617,7 +3637,7 @@ pub trait WorktreeHandle {
impl WorktreeHandle for ModelHandle<Worktree> { impl WorktreeHandle for ModelHandle<Worktree> {
// When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
// occurred before the worktree was constructed. These events can cause the worktree to perfrom // occurred before the worktree was constructed. These events can cause the worktree to perform
// extra directory scans, and emit extra scan-state notifications. // extra directory scans, and emit extra scan-state notifications.
// //
// This function mutates the worktree's directory and waits for those mutations to be picked up, // This function mutates the worktree's directory and waits for those mutations to be picked up,

View file

@ -276,7 +276,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// Set up fake langauge server to return fuzzy matches against // Set up fake language server to return fuzzy matches against
// a fixed set of symbol names. // a fixed set of symbol names.
let fake_symbols = [ let fake_symbols = [
symbol("one", "/external"), symbol("one", "/external"),

View file

@ -179,7 +179,11 @@ impl Rope {
} }
pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes { pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes {
Bytes::new(self, range) Bytes::new(self, range, false)
}
pub fn reversed_bytes_in_range(&self, range: Range<usize>) -> Bytes {
Bytes::new(self, range, true)
} }
pub fn chunks(&self) -> Chunks { pub fn chunks(&self) -> Chunks {
@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> {
pub struct Bytes<'a> { pub struct Bytes<'a> {
chunks: sum_tree::Cursor<'a, Chunk, usize>, chunks: sum_tree::Cursor<'a, Chunk, usize>,
range: Range<usize>, range: Range<usize>,
reversed: bool,
} }
impl<'a> Bytes<'a> { impl<'a> Bytes<'a> {
pub fn new(rope: &'a Rope, range: Range<usize>) -> Self { pub fn new(rope: &'a Rope, range: Range<usize>, reversed: bool) -> Self {
let mut chunks = rope.chunks.cursor(); let mut chunks = rope.chunks.cursor();
if reversed {
chunks.seek(&range.end, Bias::Left, &());
} else {
chunks.seek(&range.start, Bias::Right, &()); chunks.seek(&range.start, Bias::Right, &());
Self { chunks, range } }
Self {
chunks,
range,
reversed,
}
} }
pub fn peek(&self) -> Option<&'a [u8]> { pub fn peek(&self) -> Option<&'a [u8]> {
let chunk = self.chunks.item()?; let chunk = self.chunks.item()?;
if self.reversed && self.range.start >= self.chunks.end(&()) {
return None;
}
let chunk_start = *self.chunks.start(); let chunk_start = *self.chunks.start();
if self.range.end <= chunk_start { if self.range.end <= chunk_start {
return None; return None;
} }
let start = self.range.start.saturating_sub(chunk_start); let start = self.range.start.saturating_sub(chunk_start);
let end = self.range.end - chunk_start; let end = self.range.end - chunk_start;
Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)])
@ -607,8 +622,12 @@ impl<'a> Iterator for Bytes<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let result = self.peek(); let result = self.peek();
if result.is_some() { if result.is_some() {
if self.reversed {
self.chunks.prev(&());
} else {
self.chunks.next(&()); self.chunks.next(&());
} }
}
result result
} }
} }
@ -617,11 +636,22 @@ impl<'a> io::Read for Bytes<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if let Some(chunk) = self.peek() { if let Some(chunk) = self.peek() {
let len = cmp::min(buf.len(), chunk.len()); let len = cmp::min(buf.len(), chunk.len());
if self.reversed {
buf[..len].copy_from_slice(&chunk[chunk.len() - len..]);
buf[..len].reverse();
self.range.end -= len;
} else {
buf[..len].copy_from_slice(&chunk[..len]); buf[..len].copy_from_slice(&chunk[..len]);
self.range.start += len; self.range.start += len;
}
if len == chunk.len() { if len == chunk.len() {
if self.reversed {
self.chunks.prev(&());
} else {
self.chunks.next(&()); self.chunks.next(&());
} }
}
Ok(len) Ok(len)
} else { } else {
Ok(0) Ok(0)

View file

@ -132,6 +132,8 @@ message Envelope {
OnTypeFormatting on_type_formatting = 111; OnTypeFormatting on_type_formatting = 111;
OnTypeFormattingResponse on_type_formatting_response = 112; OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
} }
} }
@ -339,6 +341,13 @@ message UpdateWorktree {
string abs_path = 10; string abs_path = 10;
} }
message UpdateWorktreeSettings {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
optional string content = 4;
}
message CreateProjectEntry { message CreateProjectEntry {
uint64 project_id = 1; uint64 project_id = 1;
uint64 worktree_id = 2; uint64 worktree_id = 2;
@ -467,7 +476,7 @@ message Symbol {
string name = 4; string name = 4;
int32 kind = 5; int32 kind = 5;
string path = 6; string path = 6;
// Cannot use generate anchors for unopend files, // Cannot use generate anchors for unopened files,
// so we are forced to use point coords instead // so we are forced to use point coords instead
PointUtf16 start = 7; PointUtf16 start = 7;
PointUtf16 end = 8; PointUtf16 end = 8;

View file

@ -42,7 +42,7 @@ impl PublicKey {
} }
impl PrivateKey { impl PrivateKey {
/// Decrypt a base64-encoded string that was encrypted by the correspoding public key. /// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> { pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE) let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE)
.context("failed to base64-decode encrypted string")?; .context("failed to base64-decode encrypted string")?;

View file

@ -236,6 +236,7 @@ messages!(
(UpdateProject, Foreground), (UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground), (UpdateProjectCollaborator, Foreground),
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UpdateDiffBase, Foreground), (UpdateDiffBase, Foreground),
(GetPrivateUserInfo, Foreground), (GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground), (GetPrivateUserInfoResponse, Foreground),
@ -345,6 +346,7 @@ entity_messages!(
UpdateProject, UpdateProject,
UpdateProjectCollaborator, UpdateProjectCollaborator,
UpdateWorktree, UpdateWorktree,
UpdateWorktreeSettings,
UpdateDiffBase UpdateDiffBase
); );

View file

@ -25,7 +25,7 @@ use std::{
borrow::Cow, borrow::Cow,
collections::HashSet, collections::HashSet,
mem, mem,
ops::Range, ops::{Not, Range},
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
}; };
@ -242,7 +242,13 @@ impl View for ProjectSearchView {
impl Item for ProjectSearchView { impl Item for ProjectSearchView {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> { fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
Some(self.query_editor.read(cx).text(cx).into()) let query_text = self.query_editor.read(cx).text(cx);
query_text
.is_empty()
.not()
.then(|| query_text.into())
.or_else(|| Some("Project Search".into()))
} }
fn act_as_type<'a>( fn act_as_type<'a>(

View file

@ -12,7 +12,6 @@ doctest = false
test-support = ["gpui/test-support", "fs/test-support"] test-support = ["gpui/test-support", "fs/test-support"]
[dependencies] [dependencies]
assets = { path = "../assets" }
collections = { path = "../collections" } collections = { path = "../collections" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" } sqlez = { path = "../sqlez" }
@ -25,6 +24,7 @@ futures.workspace = true
json_comments = "0.2" json_comments = "0.2"
lazy_static.workspace = true lazy_static.workspace = true
postage.workspace = true postage.workspace = true
rust-embed.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true

View file

@ -1,6 +1,5 @@
use crate::settings_store::parse_json_with_comments; use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use assets::Assets;
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{keymap_matcher::Binding, AppContext}; use gpui::{keymap_matcher::Binding, AppContext};
use schemars::{ use schemars::{
@ -10,11 +9,11 @@ use schemars::{
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_json::{value::RawValue, Value}; use serde_json::{value::RawValue, Value};
use util::ResultExt; use util::{asset_str, ResultExt};
#[derive(Deserialize, Default, Clone, JsonSchema)] #[derive(Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
pub struct KeymapFileContent(Vec<KeymapBlock>); pub struct KeymapFile(Vec<KeymapBlock>);
#[derive(Deserialize, Default, Clone, JsonSchema)] #[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock { pub struct KeymapBlock {
@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
#[derive(Deserialize)] #[derive(Deserialize)]
struct ActionWithData(Box<str>, Box<RawValue>); struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent { impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
let content = Assets::get(asset_path).unwrap().data; let content = asset_str::<SettingsAssets>(asset_path);
let content_str = std::str::from_utf8(content.as_ref()).unwrap(); Self::parse(content.as_ref())?.add_to_cx(cx)
Self::parse(content_str)?.add_to_cx(cx)
} }
pub fn parse(content: &str) -> Result<Self> { pub fn parse(content: &str) -> Result<Self> {
@ -83,13 +81,12 @@ impl KeymapFileContent {
} }
Ok(()) Ok(())
} }
}
pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value { pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
let mut root_schema = SchemaSettings::draft07() let mut root_schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false) .with(|settings| settings.option_add_null_type = false)
.into_generator() .into_generator()
.into_root_schema_for::<KeymapFileContent>(); .into_root_schema_for::<KeymapFile>();
let action_schema = Schema::Object(SchemaObject { let action_schema = Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation { subschemas: Some(Box::new(SubschemaValidation {
@ -119,4 +116,5 @@ pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Val
.insert("KeymapAction".to_owned(), action_schema); .insert("KeymapAction".to_owned(), action_schema);
serde_json::to_value(root_schema).unwrap() serde_json::to_value(root_schema).unwrap()
}
} }

View file

@ -2,18 +2,37 @@ mod keymap_file;
mod settings_file; mod settings_file;
mod settings_store; mod settings_store;
use gpui::AssetSource; use rust_embed::RustEmbed;
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; use std::{borrow::Cow, str};
use util::asset_str;
pub use keymap_file::KeymapFile;
pub use settings_file::*; pub use settings_file::*;
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore}; pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
use std::{borrow::Cow, str};
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json"; #[derive(RustEmbed)]
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json"; #[folder = "../../assets"]
#[include = "settings/*"]
#[include = "keymaps/*"]
#[exclude = "*.DS_Store"]
pub struct SettingsAssets;
pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { pub fn default_settings() -> Cow<'static, str> {
match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { asset_str::<SettingsAssets>("settings/default.json")
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), }
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
} pub fn default_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>("keymaps/default.json")
}
pub fn vim_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>("keymaps/vim.json")
}
pub fn initial_user_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_user_settings.json")
}
pub fn initial_local_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_settings.json")
} }

View file

@ -1,10 +1,15 @@
use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH}; use crate::{settings_store::SettingsStore, Setting};
use anyhow::Result; use anyhow::Result;
use assets::Assets;
use fs::Fs; use fs::Fs;
use futures::{channel::mpsc, StreamExt}; use futures::{channel::mpsc, StreamExt};
use gpui::{executor::Background, AppContext, AssetSource}; use gpui::{executor::Background, AppContext};
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration}; use std::{
io::ErrorKind,
path::{Path, PathBuf},
str,
sync::Arc,
time::Duration,
};
use util::{paths, ResultExt}; use util::{paths, ResultExt};
pub fn register<T: Setting>(cx: &mut AppContext) { pub fn register<T: Setting>(cx: &mut AppContext) {
@ -17,11 +22,8 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
cx.global::<SettingsStore>().get(None) cx.global::<SettingsStore>().get(None)
} }
pub fn default_settings() -> Cow<'static, str> { pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() { cx.global::<SettingsStore>().get(location)
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
} }
pub const EMPTY_THEME_NAME: &'static str = "empty-theme"; pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
@ -29,7 +31,7 @@ pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String { pub fn test_settings() -> String {
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>( let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
default_settings().as_ref(), crate::default_settings().as_ref(),
) )
.unwrap(); .unwrap();
util::merge_non_null_json_value_into( util::merge_non_null_json_value_into(
@ -55,15 +57,22 @@ pub fn watch_config_file(
.spawn(async move { .spawn(async move {
let events = fs.watch(&path, Duration::from_millis(100)).await; let events = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events); futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop { loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await { if let Ok(contents) = fs.load(&path).await {
if !tx.unbounded_send(contents).is_ok() { if !tx.unbounded_send(contents).is_ok() {
break; break;
} }
} }
if events.next().await.is_none() {
break;
}
} }
}) })
.detach(); .detach();
@ -101,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
Err(err) => { Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() { if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound { if e.kind() == ErrorKind::NotFound {
return Ok(crate::initial_user_settings_content(&Assets).to_string()); return Ok(crate::initial_user_settings_content().to_string());
} }
} }
return Err(err); return Err(err);

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use collections::{btree_map, hash_map, BTreeMap, HashMap}; use collections::{btree_map, hash_map, BTreeMap, HashMap};
use gpui::AppContext; use gpui::AppContext;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> {
} }
/// A set of strongly-typed setting values defined via multiple JSON files. /// A set of strongly-typed setting values defined via multiple JSON files.
#[derive(Default)]
pub struct SettingsStore { pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>, setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_deserialized_settings: Option<serde_json::Value>, default_deserialized_settings: serde_json::Value,
user_deserialized_settings: Option<serde_json::Value>, user_deserialized_settings: serde_json::Value,
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>, local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>, tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
} }
impl Default for SettingsStore {
fn default() -> Self {
SettingsStore {
setting_values: Default::default(),
default_deserialized_settings: serde_json::json!({}),
user_deserialized_settings: serde_json::json!({}),
local_deserialized_settings: Default::default(),
tab_size_callback: Default::default(),
}
}
}
#[derive(Debug)] #[derive(Debug)]
struct SettingValue<T> { struct SettingValue<T> {
global_value: Option<T>, global_value: Option<T>,
local_values: Vec<(Arc<Path>, T)>, local_values: Vec<(usize, Arc<Path>, T)>,
} }
trait AnySettingValue { trait AnySettingValue {
@ -109,9 +120,9 @@ trait AnySettingValue {
custom: &[DeserializedSetting], custom: &[DeserializedSetting],
cx: &AppContext, cx: &AppContext,
) -> Result<Box<dyn Any>>; ) -> Result<Box<dyn Any>>;
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
fn set_global_value(&mut self, value: Box<dyn Any>); fn set_global_value(&mut self, value: Box<dyn Any>);
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>); fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
fn json_schema( fn json_schema(
&self, &self,
generator: &mut SchemaGenerator, generator: &mut SchemaGenerator,
@ -136,20 +147,18 @@ impl SettingsStore {
local_values: Vec::new(), local_values: Vec::new(),
})); }));
if let Some(default_settings) = &self.default_deserialized_settings {
if let Some(default_settings) = setting_value if let Some(default_settings) = setting_value
.deserialize_setting(default_settings) .deserialize_setting(&self.default_deserialized_settings)
.log_err() .log_err()
{ {
let mut user_values_stack = Vec::new(); let mut user_values_stack = Vec::new();
if let Some(user_settings) = &self.user_deserialized_settings { if let Some(user_settings) = setting_value
if let Some(user_settings) = .deserialize_setting(&self.user_deserialized_settings)
setting_value.deserialize_setting(user_settings).log_err() .log_err()
{ {
user_values_stack = vec![user_settings]; user_values_stack = vec![user_settings];
} }
}
if let Some(setting) = setting_value if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx) .load_setting(&default_settings, &user_values_stack, cx)
@ -159,13 +168,12 @@ impl SettingsStore {
} }
} }
} }
}
/// Get the value of a setting. /// Get the value of a setting.
/// ///
/// Panics if the given setting type has not been registered, or if there is no /// Panics if the given setting type has not been registered, or if there is no
/// value for this setting. /// value for this setting.
pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T { pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
self.setting_values self.setting_values
.get(&TypeId::of::<T>()) .get(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>())) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@ -189,9 +197,7 @@ impl SettingsStore {
/// This is only for debugging and reporting. For user-facing functionality, /// This is only for debugging and reporting. For user-facing functionality,
/// use the typed setting interface. /// use the typed setting interface.
pub fn untyped_user_settings(&self) -> &serde_json::Value { pub fn untyped_user_settings(&self) -> &serde_json::Value {
self.user_deserialized_settings &self.user_deserialized_settings
.as_ref()
.unwrap_or(&serde_json::Value::Null)
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -213,11 +219,7 @@ impl SettingsStore {
cx: &AppContext, cx: &AppContext,
update: impl FnOnce(&mut T::FileContent), update: impl FnOnce(&mut T::FileContent),
) { ) {
if self.user_deserialized_settings.is_none() { let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
self.set_user_settings("{}", cx).unwrap();
}
let old_text =
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
let new_text = self.new_text_for_update::<T>(old_text, update); let new_text = self.new_text_for_update::<T>(old_text, update);
self.set_user_settings(&new_text, cx).unwrap(); self.set_user_settings(&new_text, cx).unwrap();
} }
@ -250,11 +252,7 @@ impl SettingsStore {
.setting_values .setting_values
.get(&setting_type_id) .get(&setting_type_id)
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>())) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.deserialize_setting( .deserialize_setting(&self.user_deserialized_settings)
self.user_deserialized_settings
.as_ref()
.expect("no user settings loaded"),
)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!( panic!(
"could not deserialize setting type {} from user settings: {}", "could not deserialize setting type {} from user settings: {}",
@ -323,10 +321,14 @@ impl SettingsStore {
default_settings_content: &str, default_settings_content: &str,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
self.default_deserialized_settings = let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
Some(parse_json_with_comments(default_settings_content)?); if settings.is_object() {
self.default_deserialized_settings = settings;
self.recompute_values(None, cx)?; self.recompute_values(None, cx)?;
Ok(()) Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
} }
/// Set the user settings via a JSON string. /// Set the user settings via a JSON string.
@ -335,28 +337,49 @@ impl SettingsStore {
user_settings_content: &str, user_settings_content: &str,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
if settings.is_object() {
self.user_deserialized_settings = settings;
self.recompute_values(None, cx)?; self.recompute_values(None, cx)?;
Ok(()) Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
} }
/// Add or remove a set of local settings via a JSON string. /// Add or remove a set of local settings via a JSON string.
pub fn set_local_settings( pub fn set_local_settings(
&mut self, &mut self,
root_id: usize,
path: Arc<Path>, path: Arc<Path>,
settings_content: Option<&str>, settings_content: Option<&str>,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
if let Some(content) = settings_content { if let Some(content) = settings_content {
self.local_deserialized_settings self.local_deserialized_settings
.insert(path.clone(), parse_json_with_comments(content)?); .insert((root_id, path.clone()), parse_json_with_comments(content)?);
} else { } else {
self.local_deserialized_settings.remove(&path); self.local_deserialized_settings
.remove(&(root_id, path.clone()));
} }
self.recompute_values(Some(&path), cx)?; self.recompute_values(Some((root_id, &path)), cx)?;
Ok(()) Ok(())
} }
/// Add or remove a set of local settings via a JSON string.
pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
self.local_deserialized_settings
.retain(|k, _| k.0 != root_id);
self.recompute_values(Some((root_id, "".as_ref())), cx)?;
Ok(())
}
pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
self.local_deserialized_settings
.range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
}
pub fn json_schema( pub fn json_schema(
&self, &self,
schema_params: &SettingsJsonSchemaParams, schema_params: &SettingsJsonSchemaParams,
@ -436,27 +459,26 @@ impl SettingsStore {
fn recompute_values( fn recompute_values(
&mut self, &mut self,
changed_local_path: Option<&Path>, changed_local_path: Option<(usize, &Path)>,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
// Reload the global and local values for every setting. // Reload the global and local values for every setting.
let mut user_settings_stack = Vec::<DeserializedSetting>::new(); let mut user_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<&Path>>::new(); let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
for setting_value in self.setting_values.values_mut() { for setting_value in self.setting_values.values_mut() {
if let Some(default_settings) = &self.default_deserialized_settings { let default_settings =
let default_settings = setting_value.deserialize_setting(default_settings)?; setting_value.deserialize_setting(&self.default_deserialized_settings)?;
user_settings_stack.clear(); user_settings_stack.clear();
paths_stack.clear(); paths_stack.clear();
if let Some(user_settings) = &self.user_deserialized_settings { if let Some(user_settings) = setting_value
if let Some(user_settings) = .deserialize_setting(&self.user_deserialized_settings)
setting_value.deserialize_setting(user_settings).log_err() .log_err()
{ {
user_settings_stack.push(user_settings); user_settings_stack.push(user_settings);
paths_stack.push(None); paths_stack.push(None);
} }
}
// If the global settings file changed, reload the global value for the field. // If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() { if changed_local_path.is_none() {
@ -469,11 +491,11 @@ impl SettingsStore {
} }
// Reload the local values for the setting. // Reload the local values for the setting.
for (path, local_settings) in &self.local_deserialized_settings { for ((root_id, path), local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting. // Build a stack of all of the local values for that setting.
while let Some(prev_path) = paths_stack.last() { while let Some(prev_entry) = paths_stack.last() {
if let Some(prev_path) = prev_path { if let Some((prev_root_id, prev_path)) = prev_entry {
if !path.starts_with(prev_path) { if root_id != prev_root_id || !path.starts_with(prev_path) {
paths_stack.pop(); paths_stack.pop();
user_settings_stack.pop(); user_settings_stack.pop();
continue; continue;
@ -485,13 +507,13 @@ impl SettingsStore {
if let Some(local_settings) = if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err() setting_value.deserialize_setting(&local_settings).log_err()
{ {
paths_stack.push(Some(path.as_ref())); paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings); user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local // If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory. // settings for any path outside of that directory.
if changed_local_path.map_or(false, |changed_local_path| { if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
!path.starts_with(changed_local_path) *root_id != changed_root_id || !path.starts_with(changed_local_path)
}) { }) {
continue; continue;
} }
@ -500,8 +522,7 @@ impl SettingsStore {
.load_setting(&default_settings, &user_settings_stack, cx) .load_setting(&default_settings, &user_settings_stack, cx)
.log_err() .log_err()
{ {
setting_value.set_local_value(path.clone(), value); setting_value.set_local_value(*root_id, path.clone(), value);
}
} }
} }
} }
@ -510,6 +531,24 @@ impl SettingsStore {
} }
} }
impl Debug for SettingsStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SettingsStore")
.field(
"types",
&self
.setting_values
.values()
.map(|value| value.setting_type_name())
.collect::<Vec<_>>(),
)
.field("default_settings", &self.default_deserialized_settings)
.field("user_settings", &self.user_deserialized_settings)
.field("local_settings", &self.local_deserialized_settings)
.finish_non_exhaustive()
}
}
impl<T: Setting> AnySettingValue for SettingValue<T> { impl<T: Setting> AnySettingValue for SettingValue<T> {
fn key(&self) -> Option<&'static str> { fn key(&self) -> Option<&'static str> {
T::KEY T::KEY
@ -546,10 +585,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
Ok(DeserializedSetting(Box::new(value))) Ok(DeserializedSetting(Box::new(value)))
} }
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any { fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
if let Some(path) = path { if let Some((root_id, path)) = path {
for (settings_path, value) in self.local_values.iter().rev() { for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
if path.starts_with(&settings_path) { if root_id == *settings_root_id && path.starts_with(&settings_path) {
return value; return value;
} }
} }
@ -563,11 +602,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
self.global_value = Some(*value.downcast().unwrap()); self.global_value = Some(*value.downcast().unwrap());
} }
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) { fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
let value = *value.downcast().unwrap(); let value = *value.downcast().unwrap();
match self.local_values.binary_search_by_key(&&path, |e| &e.0) { match self
Ok(ix) => self.local_values[ix].1 = value, .local_values
Err(ix) => self.local_values.insert(ix, (path, value)), .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
{
Ok(ix) => self.local_values[ix].2 = value,
Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
} }
} }
@ -581,22 +623,6 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
} }
} }
// impl Debug for SettingsStore {
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// return f
// .debug_struct("SettingsStore")
// .field(
// "setting_value_sets_by_type",
// &self
// .setting_values
// .values()
// .map(|set| (set.setting_type_name(), set))
// .collect::<HashMap<_, _>>(),
// )
// .finish_non_exhaustive();
// }
// }
fn update_value_in_json_text<'a>( fn update_value_in_json_text<'a>(
text: &mut String, text: &mut String,
key_path: &mut Vec<&'a str>, key_path: &mut Vec<&'a str>,
@ -639,6 +665,10 @@ fn update_value_in_json_text<'a>(
key_path.pop(); key_path.pop();
} }
} else if old_value != new_value { } else if old_value != new_value {
let mut new_value = new_value.clone();
if let Some(new_object) = new_value.as_object_mut() {
new_object.retain(|_, v| !v.is_null());
}
let (range, replacement) = let (range, replacement) =
replace_value_in_json_text(text, &key_path, tab_size, &new_value); replace_value_in_json_text(text, &key_path, tab_size, &new_value);
text.replace_range(range.clone(), &replacement); text.replace_range(range.clone(), &replacement);
@ -650,7 +680,7 @@ fn replace_value_in_json_text(
text: &str, text: &str,
key_path: &[&str], key_path: &[&str],
tab_size: usize, tab_size: usize,
new_value: impl Serialize, new_value: &serde_json::Value,
) -> (Range<usize>, String) { ) -> (Range<usize>, String) {
const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
const LANGUAGES: &'static str = "languages"; const LANGUAGES: &'static str = "languages";
@ -884,6 +914,7 @@ mod tests {
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root1").into(), Path::new("/root1").into(),
Some(r#"{ "user": { "staff": true } }"#), Some(r#"{ "user": { "staff": true } }"#),
cx, cx,
@ -891,6 +922,7 @@ mod tests {
.unwrap(); .unwrap();
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root1/subdir").into(), Path::new("/root1/subdir").into(),
Some(r#"{ "user": { "name": "Jane Doe" } }"#), Some(r#"{ "user": { "name": "Jane Doe" } }"#),
cx, cx,
@ -899,6 +931,7 @@ mod tests {
store store
.set_local_settings( .set_local_settings(
1,
Path::new("/root2").into(), Path::new("/root2").into(),
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
cx, cx,
@ -906,7 +939,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/something"))), store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
&UserSettings { &UserSettings {
name: "John Doe".to_string(), name: "John Doe".to_string(),
age: 31, age: 31,
@ -914,7 +947,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))), store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
&UserSettings { &UserSettings {
name: "Jane Doe".to_string(), name: "Jane Doe".to_string(),
age: 31, age: 31,
@ -922,7 +955,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root2/something"))), store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
&UserSettings { &UserSettings {
name: "John Doe".to_string(), name: "John Doe".to_string(),
age: 42, age: 42,
@ -930,7 +963,7 @@ mod tests {
} }
); );
assert_eq!( assert_eq!(
store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))), store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
&MultiKeySettings { &MultiKeySettings {
key1: "a".to_string(), key1: "a".to_string(),
key2: "b".to_string(), key2: "b".to_string(),
@ -994,24 +1027,32 @@ mod tests {
r#"{ r#"{
"languages": { "languages": {
"JSON": { "JSON": {
"is_enabled": true "language_setting_1": true
} }
} }
}"# }"#
.unindent(), .unindent(),
|settings| { |settings| {
settings.languages.get_mut("JSON").unwrap().is_enabled = false;
settings settings
.languages .languages
.insert("Rust".into(), LanguageSettingEntry { is_enabled: true }); .get_mut("JSON")
.unwrap()
.language_setting_1 = Some(false);
settings.languages.insert(
"Rust".into(),
LanguageSettingEntry {
language_setting_2: Some(true),
..Default::default()
},
);
}, },
r#"{ r#"{
"languages": { "languages": {
"Rust": { "Rust": {
"is_enabled": true "language_setting_2": true
}, },
"JSON": { "JSON": {
"is_enabled": false "language_setting_1": false
} }
} }
}"# }"#
@ -1074,6 +1115,23 @@ mod tests {
.unindent(), .unindent(),
cx, cx,
); );
check_settings_update::<UserSettings>(
&mut store,
r#"{
}
"#
.unindent(),
|settings| settings.age = Some(37),
r#"{
"user": {
"age": 37
}
}
"#
.unindent(),
cx,
);
} }
fn check_settings_update<T: Setting>( fn check_settings_update<T: Setting>(
@ -1202,9 +1260,10 @@ mod tests {
languages: HashMap<String, LanguageSettingEntry>, languages: HashMap<String, LanguageSettingEntry>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
struct LanguageSettingEntry { struct LanguageSettingEntry {
is_enabled: bool, language_setting_1: Option<bool>,
language_setting_2: Option<bool>,
} }
impl Setting for LanguageSettings { impl Setting for LanguageSettings {

View file

@ -160,7 +160,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
// Create a one shot channel for the result of the queued write // Create a one shot channel for the result of the queued write
// so we can await on the result // so we can await on the result
let (sender, reciever) = oneshot::channel(); let (sender, receiver) = oneshot::channel();
let thread_safe_connection = (*self).clone(); let thread_safe_connection = (*self).clone();
write_channel(Box::new(move || { write_channel(Box::new(move || {
@ -168,7 +168,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
let result = connection.with_write(|connection| callback(connection)); let result = connection.with_write(|connection| callback(connection));
sender.send(result).ok(); sender.send(result).ok();
})); }));
reciever.map(|response| response.expect("Write queue unexpectedly closed")) receiver.map(|response| response.expect("Write queue unexpectedly closed"))
} }
pub(crate) fn create_connection( pub(crate) fn create_connection(
@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor {
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
Box::new(|| { Box::new(|| {
let (sender, reciever) = channel::<QueuedWrite>(); let (sender, receiver) = channel::<QueuedWrite>();
thread::spawn(move || { thread::spawn(move || {
while let Ok(write) = reciever.recv() { while let Ok(write) = receiver.recv() {
write() write()
} }
}); });

View file

@ -45,7 +45,7 @@ pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
} }
///Converts an 8 bit ANSI color to it's GPUI equivalent. ///Converts an 8 bit ANSI color to it's GPUI equivalent.
///Accepts usize for compatability with the alacritty::Colors interface, ///Accepts usize for compatibility with the alacritty::Colors interface,
///Other than that use case, should only be called with values in the [0,255] range ///Other than that use case, should only be called with values in the [0,255] range
pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
match index { match index {
@ -78,7 +78,7 @@ pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
} }
//For compatability with the alacritty::Colors interface //For compatibility with the alacritty::Colors interface
256 => style.foreground, 256 => style.foreground,
257 => style.background, 257 => style.background,
258 => style.cursor, 258 => style.cursor,

View file

@ -37,8 +37,6 @@ lazy_static.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }

View file

@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal:
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. 3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a seperate pathway. 4. Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View file

@ -40,7 +40,7 @@ function contrast_colour {
# Uncomment the below for more precise luminance calculations # Uncomment the below for more precise luminance calculations
# # Calculate percieved brightness # # Calculate perceived brightness
# # See https://www.w3.org/TR/AERT#color-contrast # # See https://www.w3.org/TR/AERT#color-contrast
# # and http://www.itu.int/rec/R-REC-BT.601 # # and http://www.itu.int/rec/R-REC-BT.601
# # Luminance is in range 0..5000 as each value is 0..5 # # Luminance is in range 0..5000 as each value is 0..5

View file

@ -34,7 +34,7 @@ use std::{mem, ops::Range};
use crate::TerminalView; use crate::TerminalView;
///The information generated during layout that is nescessary for painting ///The information generated during layout that is necessary for painting
pub struct LayoutState { pub struct LayoutState {
cells: Vec<LayoutCell>, cells: Vec<LayoutCell>,
rects: Vec<LayoutRect>, rects: Vec<LayoutRect>,
@ -206,7 +206,7 @@ impl TerminalElement {
//Expand background rect range //Expand background rect range
{ {
if matches!(bg, Named(NamedColor::Background)) { if matches!(bg, Named(NamedColor::Background)) {
//Continue to next cell, resetting variables if nescessary //Continue to next cell, resetting variables if necessary
cur_alac_color = None; cur_alac_color = None;
if let Some(rect) = cur_rect { if let Some(rect) = cur_rect {
rects.push(rect); rects.push(rect);

View file

@ -70,6 +70,7 @@ impl TerminalPanel {
.with_child(Pane::render_tab_bar_button( .with_child(Pane::render_tab_bar_button(
0, 0,
"icons/plus_12.svg", "icons/plus_12.svg",
false,
Some(( Some((
"New Terminal".into(), "New Terminal".into(),
Some(Box::new(workspace::NewTerminal)), Some(Box::new(workspace::NewTerminal)),
@ -94,6 +95,7 @@ impl TerminalPanel {
} else { } else {
"icons/maximize_8.svg" "icons/maximize_8.svg"
}, },
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),

View file

@ -133,8 +133,8 @@ impl TerminalView {
Event::Wakeup => { Event::Wakeup => {
if !cx.is_self_focused() { if !cx.is_self_focused() {
this.has_new_content = true; this.has_new_content = true;
cx.notify();
} }
cx.notify();
cx.emit(Event::Wakeup); cx.emit(Event::Wakeup);
} }
Event::Bell => { Event::Bell => {
@ -804,7 +804,7 @@ mod tests {
let workspace = workspace.read(cx); let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry(); let active_entry = project.read(cx).active_entry();
//Make sure enviroment is as expeted //Make sure environment is as expected
assert!(active_entry.is_none()); assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_none()); assert!(workspace.worktrees(cx).next().is_none());
@ -825,7 +825,7 @@ mod tests {
let workspace = workspace.read(cx); let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry(); let active_entry = project.read(cx).active_entry();
//Make sure enviroment is as expeted //Make sure environment is as expected
assert!(active_entry.is_none()); assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_some()); assert!(workspace.worktrees(cx).next().is_some());
@ -905,7 +905,10 @@ mod tests {
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> (ModelHandle<Project>, ViewHandle<Workspace>) { ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
let params = cx.update(AppState::test); let params = cx.update(AppState::test);
cx.update(|cx| theme::init((), cx)); cx.update(|cx| {
theme::init((), cx);
language::init(cx);
});
let project = Project::test(params.fs.clone(), [], cx).await; let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));

View file

@ -193,7 +193,7 @@ fn test_line_len() {
} }
#[test] #[test]
fn test_common_prefix_at_positionn() { fn test_common_prefix_at_position() {
let text = "a = str; b = δα"; let text = "a = str; b = δα";
let buffer = Buffer::new(0, 0, text.into()); let buffer = Buffer::new(0, 0, text.into());
@ -216,7 +216,7 @@ fn test_common_prefix_at_positionn() {
empty_range_after(text, "str"), empty_range_after(text, "str"),
); );
// prefix matching is case insenstive. // prefix matching is case insensitive.
assert_eq!( assert_eq!(
buffer.common_prefix_at(offset1, "Strαngε"), buffer.common_prefix_at(offset1, "Strαngε"),
range_of(text, "str"), range_of(text, "str"),

View file

@ -1749,6 +1749,12 @@ impl BufferSnapshot {
self.visible_text.bytes_in_range(start..end) self.visible_text.bytes_in_range(start..end)
} }
pub fn reversed_bytes_in_range<T: ToOffset>(&self, range: Range<T>) -> rope::Bytes<'_> {
let start = range.start.to_offset(self);
let end = range.end.to_offset(self);
self.visible_text.reversed_bytes_in_range(start..end)
}
pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Chunks<'_> { pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Chunks<'_> {
let start = range.start.to_offset(self); let start = range.start.to_offset(self);
let end = range.end.to_offset(self); let end = range.end.to_offset(self);

View file

@ -60,6 +60,7 @@ pub struct Theme {
pub incoming_call_notification: IncomingCallNotification, pub incoming_call_notification: IncomingCallNotification,
pub tooltip: TooltipStyle, pub tooltip: TooltipStyle,
pub terminal: TerminalStyle, pub terminal: TerminalStyle,
pub assistant: AssistantStyle,
pub feedback: FeedbackStyle, pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle, pub welcome: WelcomeStyle,
pub color_scheme: ColorScheme, pub color_scheme: ColorScheme,
@ -968,6 +969,23 @@ pub struct TerminalStyle {
pub dim_foreground: Color, pub dim_foreground: Color,
} }
#[derive(Clone, Deserialize, Default)]
pub struct AssistantStyle {
pub container: ContainerStyle,
pub header: ContainerStyle,
pub sent_at: ContainedText,
pub user_sender: Interactive<ContainedText>,
pub assistant_sender: Interactive<ContainedText>,
pub system_sender: Interactive<ContainedText>,
pub model_info_container: ContainerStyle,
pub model: Interactive<ContainedText>,
pub remaining_tokens: ContainedText,
pub no_remaining_tokens: ContainedText,
pub error_icon: Icon,
pub api_key_editor: FieldEditor,
pub api_key_prompt: ContainedText,
}
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
pub struct FeedbackStyle { pub struct FeedbackStyle {
pub submit_button: Interactive<ContainedText>, pub submit_button: Interactive<ContainedText>,

View file

@ -21,6 +21,7 @@ isahc.workspace = true
smol.workspace = true smol.workspace = true
url = "2.2" url = "2.2"
rand.workspace = true rand.workspace = true
rust-embed.workspace = true
tempdir = { workspace = true, optional = true } tempdir = { workspace = true, optional = true }
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true

View file

@ -15,6 +15,7 @@ lazy_static::lazy_static! {
pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
} }
pub mod legacy { pub mod legacy {

View file

@ -7,8 +7,10 @@ pub mod paths;
pub mod test; pub mod test;
use std::{ use std::{
borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
ops::{AddAssign, Range, RangeInclusive}, ops::{AddAssign, Range, RangeInclusive},
panic::Location,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -129,11 +131,13 @@ where
{ {
type Ok = T; type Ok = T;
#[track_caller]
fn log_err(self) -> Option<T> { fn log_err(self) -> Option<T> {
match self { match self {
Ok(value) => Some(value), Ok(value) => Some(value),
Err(error) => { Err(error) => {
log::error!("{:?}", error); let caller = Location::caller();
log::error!("{}:{}: {:?}", caller.file(), caller.line(), error);
None None
} }
} }
@ -281,6 +285,14 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
} }
} }
/// Get an embedded file as a string.
pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
match A::get(path).unwrap().data {
Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
}
}
// copy unstable standard feature option unzip // copy unstable standard feature option unzip
// https://github.com/rust-lang/rust/issues/87800 // https://github.com/rust-lang/rust/issues/87800
// Remove when this ship in Rust 1.66 or 1.67 // Remove when this ship in Rust 1.66 or 1.67

View file

@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f
tokio = { version = "1.15", "optional" = true } tokio = { version = "1.15", "optional" = true }
serde_json.workspace = true serde_json.workspace = true
assets = { path = "../assets" }
collections = { path = "../collections" } collections = { path = "../collections" }
command_palette = { path = "../command_palette" } command_palette = { path = "../command_palette" }
editor = { path = "../editor" } editor = { path = "../editor" }

View file

@ -400,7 +400,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Edito
}; };
let scroll_margin_rows = editor.vertical_scroll_margin() as u32; let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
let top_anchor = editor.scroll_manager.anchor().top_anchor; let top_anchor = editor.scroll_manager.anchor().anchor;
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
s.replace_cursors_with(|snapshot| { s.replace_cursors_with(|snapshot| {
@ -756,7 +756,7 @@ mod test {
ˇ ˇ
The quick"}) The quick"})
.await; .await;
// Indoc disallows trailing whitspace. // Indoc disallows trailing whitespace.
cx.assert(" ˇ \nThe quick").await; cx.assert(" ˇ \nThe quick").await;
} }

View file

@ -29,7 +29,7 @@ use tokio::{
use crate::state::Mode; use crate::state::Mode;
use collections::VecDeque; use collections::VecDeque;
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock // Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
// to ensure we are only constructing one neovim connection at a time. // to ensure we are only constructing one neovim connection at a time.
#[cfg(feature = "neovim")] #[cfg(feature = "neovim")]
lazy_static! { lazy_static! {

View file

@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
cx.update_global(|store: &mut SettingsStore, cx| { cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled)); store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
}); });
settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap(); settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
}); });
// Setup search toolbars and keypress hook // Setup search toolbars and keypress hook

View file

@ -19,7 +19,6 @@ test-support = [
] ]
[dependencies] [dependencies]
assets = { path = "../assets" }
db = { path = "../db" } db = { path = "../db" }
call = { path = "../call" } call = { path = "../call" }
client = { path = "../client" } client = { path = "../client" }

View file

@ -175,6 +175,10 @@ impl Dock {
} }
} }
pub fn position(&self) -> DockPosition {
self.position
}
pub fn is_open(&self) -> bool { pub fn is_open(&self) -> bool {
self.is_open self.is_open
} }
@ -184,6 +188,12 @@ impl Dock {
.map_or(false, |panel| panel.has_focus(cx)) .map_or(false, |panel| panel.has_focus(cx))
} }
pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
self.panel_entries
.iter()
.find_map(|entry| entry.panel.as_any().clone().downcast())
}
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> { pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries self.panel_entries
.iter() .iter()
@ -472,11 +482,22 @@ impl View for PanelButtons {
Flex::row() Flex::row()
.with_children(panels.into_iter().enumerate().map( .with_children(panels.into_iter().enumerate().map(
|(panel_ix, (view, context_menu))| { |(panel_ix, (view, context_menu))| {
let (tooltip, tooltip_action) = view.icon_tooltip(cx); let is_active = is_open && panel_ix == active_ix;
let (tooltip, tooltip_action) = if is_active {
(
format!("Close {} dock", dock_position.to_label()),
Some(match dock_position {
DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
}),
)
} else {
view.icon_tooltip(cx)
};
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| { MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
let is_active = is_open && panel_ix == active_ix;
let style = button_style.style_for(state, is_active); let style = button_style.style_for(state, is_active);
Flex::row() Flex::row()
.with_child( .with_child(

View file

@ -1,5 +1,5 @@
use crate::{Toast, Workspace}; use crate::{Toast, Workspace};
use collections::HashSet; use collections::HashMap;
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
use std::{any::TypeId, ops::DerefMut}; use std::{any::TypeId, ops::DerefMut};
@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
} }
} }
struct NotificationTracker { pub(crate) struct NotificationTracker {
notifications_sent: HashSet<TypeId>, notifications_sent: HashMap<TypeId, Vec<usize>>,
} }
impl std::ops::Deref for NotificationTracker { impl std::ops::Deref for NotificationTracker {
type Target = HashSet<TypeId>; type Target = HashMap<TypeId, Vec<usize>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.notifications_sent &self.notifications_sent
@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
impl NotificationTracker { impl NotificationTracker {
fn new() -> Self { fn new() -> Self {
Self { Self {
notifications_sent: HashSet::default(), notifications_sent: Default::default(),
} }
} }
} }
impl Workspace { impl Workspace {
pub fn has_shown_notification_once<V: Notification>(
&self,
id: usize,
cx: &ViewContext<Self>,
) -> bool {
cx.global::<NotificationTracker>()
.get(&TypeId::of::<V>())
.map(|ids| ids.contains(&id))
.unwrap_or(false)
}
pub fn show_notification_once<V: Notification>( pub fn show_notification_once<V: Notification>(
&mut self, &mut self,
id: usize, id: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>, build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) { ) {
if !cx if !self.has_shown_notification_once::<V>(id, cx) {
.global::<NotificationTracker>()
.contains(&TypeId::of::<V>())
{
cx.update_global::<NotificationTracker, _, _>(|tracker, _| { cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
tracker.insert(TypeId::of::<V>()) let entry = tracker.entry(TypeId::of::<V>()).or_default();
entry.push(id);
}); });
self.show_notification::<V>(id, cx, build_notification) self.show_notification::<V>(id, cx, build_notification)
@ -154,9 +163,10 @@ pub mod simple_message_notification {
use gpui::{ use gpui::{
actions, actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
fonts::TextStyle,
impl_actions, impl_actions,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AppContext, Element, Entity, View, ViewContext, AnyElement, AppContext, Element, Entity, View, ViewContext,
}; };
use menu::Cancel; use menu::Cancel;
use serde::Deserialize; use serde::Deserialize;
@ -184,8 +194,13 @@ pub mod simple_message_notification {
) )
} }
enum NotificationMessage {
Text(Cow<'static, str>),
Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
}
pub struct MessageNotification { pub struct MessageNotification {
message: Cow<'static, str>, message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>, on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>, click_message: Option<Cow<'static, str>>,
} }
@ -204,7 +219,17 @@ pub mod simple_message_notification {
S: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>,
{ {
Self { Self {
message: message.into(), message: NotificationMessage::Text(message.into()),
on_click: None,
click_message: None,
}
}
pub fn new_element(
message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
) -> MessageNotification {
Self {
message: NotificationMessage::Element(message),
on_click: None, on_click: None,
click_message: None, click_message: None,
} }
@ -243,16 +268,20 @@ pub mod simple_message_notification {
enum MessageNotificationTag {} enum MessageNotificationTag {}
let click_message = self.click_message.clone(); let click_message = self.click_message.clone();
let message = self.message.clone(); let message = match &self.message {
NotificationMessage::Text(text) => {
Text::new(text.to_owned(), theme.message.text.clone()).into_any()
}
NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
};
let on_click = self.on_click.clone(); let on_click = self.on_click.clone();
let has_click_action = on_click.is_some(); let has_click_action = on_click.is_some();
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
Flex::column() Flex::column()
.with_child( .with_child(
Flex::row() Flex::row()
.with_child( .with_child(
Text::new(message, theme.message.text.clone()) message
.contained() .contained()
.with_style(theme.message.container) .with_style(theme.message.container)
.aligned() .aligned()
@ -281,45 +310,47 @@ pub mod simple_message_notification {
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.aligned() .aligned()
.constrained() .constrained()
.with_height( .with_height(cx.font_cache().line_height(theme.message.text.font_size))
cx.font_cache().line_height(theme.message.text.font_size),
)
.aligned() .aligned()
.top() .top()
.flex_float(), .flex_float(),
), ),
) )
.with_children({ .with_children({
click_message
.map(|click_message| {
MouseEventHandler::<MessageNotificationTag, _>::new(
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state, false); let style = theme.action_message.style_for(state, false);
if let Some(click_message) = click_message {
Some( Flex::row()
Flex::row().with_child( .with_child(
Text::new(click_message, style.text.clone()) Text::new(click_message, style.text.clone())
.contained() .contained()
.with_style(style.container), .with_style(style.container),
),
) )
} else {
None
}
.into_iter()
})
.contained() .contained()
}) },
// Since we're not using a proper overlay, we have to capture these extra events )
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(on_click) = on_click.as_ref() { if let Some(on_click) = on_click.as_ref() {
on_click(cx); on_click(cx);
this.dismiss(&Default::default(), cx); this.dismiss(&Default::default(), cx);
} }
}) })
// Since we're not using a proper overlay, we have to capture these extra events
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.with_cursor_style(if has_click_action { .with_cursor_style(if has_click_action {
CursorStyle::PointingHand CursorStyle::PointingHand
} else { } else {
CursorStyle::Arrow CursorStyle::Arrow
}) })
})
.into_iter()
})
.into_any() .into_any()
} }
} }

View file

@ -2,8 +2,8 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection}; use super::{ItemHandle, SplitDirection};
use crate::{ use crate::{
item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile, item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
NewSearch, ToggleZoom, Workspace, WorkspaceSettings, NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
}; };
use anyhow::Result; use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque}; use collections::{HashMap, HashSet, VecDeque};
@ -268,6 +268,7 @@ impl Pane {
.with_child(Self::render_tab_bar_button( .with_child(Self::render_tab_bar_button(
0, 0,
"icons/plus_12.svg", "icons/plus_12.svg",
false,
Some(("New...".into(), None)), Some(("New...".into(), None)),
cx, cx,
|pane, cx| pane.deploy_new_menu(cx), |pane, cx| pane.deploy_new_menu(cx),
@ -277,6 +278,7 @@ impl Pane {
.with_child(Self::render_tab_bar_button( .with_child(Self::render_tab_bar_button(
1, 1,
"icons/split_12.svg", "icons/split_12.svg",
false,
Some(("Split Pane".into(), None)), Some(("Split Pane".into(), None)),
cx, cx,
|pane, cx| pane.deploy_split_menu(cx), |pane, cx| pane.deploy_split_menu(cx),
@ -290,6 +292,7 @@ impl Pane {
} else { } else {
"icons/maximize_8.svg" "icons/maximize_8.svg"
}, },
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
@ -536,6 +539,11 @@ impl Pane {
} }
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) { pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
// Potentially warn the user of the new keybinding
let workspace_handle = self.workspace().clone();
cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
.detach();
if self.zoomed { if self.zoomed {
cx.emit(Event::ZoomOut); cx.emit(Event::ZoomOut);
} else if !self.items.is_empty() { } else if !self.items.is_empty() {
@ -1014,7 +1022,7 @@ impl Pane {
let is_active_item = target_item_id == active_item_id; let is_active_item = target_item_id == active_item_id;
let target_pane = cx.weak_handle(); let target_pane = cx.weak_handle();
// The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
self.tab_context_menu.update(cx, |menu, cx| { self.tab_context_menu.update(cx, |menu, cx| {
menu.show( menu.show(
@ -1143,7 +1151,8 @@ impl Pane {
let theme = theme::current(cx).clone(); let theme = theme::current(cx).clone();
let mut tooltip_theme = theme.tooltip.clone(); let mut tooltip_theme = theme.tooltip.clone();
tooltip_theme.max_text_width = None; tooltip_theme.max_text_width = None;
let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); let tab_tooltip_text =
item.tab_tooltip_text(cx).map(|text| text.into_owned());
move |mouse_state, cx| { move |mouse_state, cx| {
let tab_style = let tab_style =
@ -1401,6 +1410,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>( pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize, index: usize,
icon: &'static str, icon: &'static str,
active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>, tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
on_click: F, on_click: F,
@ -1410,7 +1420,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| { let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar; let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, false); let style = theme.pane_button.style_for(mouse_state, active);
Svg::new(icon) Svg::new(icon)
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()

View file

@ -15,7 +15,6 @@ mod toolbar;
mod workspace_settings; mod workspace_settings;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use assets::Assets;
use call::ActiveCall; use call::ActiveCall;
use client::{ use client::{
proto::{self, PeerId}, proto::{self, PeerId},
@ -60,7 +59,7 @@ use std::{
}; };
use crate::{ use crate::{
notifications::simple_message_notification::MessageNotification, notifications::{simple_message_notification::MessageNotification, NotificationTracker},
persistence::model::{ persistence::model::{
DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
}, },
@ -81,9 +80,9 @@ use serde::Deserialize;
use shared_screen::SharedScreen; use shared_screen::SharedScreen;
use status_bar::StatusBar; use status_bar::StatusBar;
pub use status_bar::StatusItemView; pub use status_bar::StatusItemView;
use theme::Theme; use theme::{Theme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::{async_iife, paths, ResultExt}; use util::{async_iife, ResultExt};
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! { lazy_static! {
@ -133,8 +132,6 @@ actions!(
] ]
); );
actions!(zed, [OpenSettings]);
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct OpenPaths { pub struct OpenPaths {
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
@ -295,17 +292,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach(); .detach();
}); });
cx.add_action(
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content(&Assets)
.as_ref()
.into()
})
.detach_and_log_err(cx);
},
);
let client = &app_state.client; let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow); client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow); client.add_view_message_handler(Workspace::handle_unfollow);
@ -765,25 +751,21 @@ impl Workspace {
DB.next_id().await.unwrap_or(0) DB.next_id().await.unwrap_or(0)
}; };
let window_bounds_override =
ZED_WINDOW_POSITION
.zip(*ZED_WINDOW_SIZE)
.map(|(position, size)| {
WindowBounds::Fixed(RectF::new(
cx.platform().screens()[0].bounds().origin() + position,
size,
))
});
let build_workspace = |cx: &mut ViewContext<Workspace>| {
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
};
let workspace = requesting_window_id let workspace = requesting_window_id
.and_then(|window_id| { .and_then(|window_id| {
cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx))) cx.update(|cx| {
cx.replace_root_view(window_id, |cx| {
Workspace::new(
workspace_id,
project_handle.clone(),
app_state.clone(),
cx,
)
})
})
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
let window_bounds_override = window_bounds_env_override(&cx);
let (bounds, display) = if let Some(bounds) = window_bounds_override { let (bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None) (Some(bounds), None)
} else { } else {
@ -819,7 +801,14 @@ impl Workspace {
// Use the serialized workspace to construct the new window // Use the serialized workspace to construct the new window
cx.add_window( cx.add_window(
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()), (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|cx| build_workspace(cx), |cx| {
Workspace::new(
workspace_id,
project_handle.clone(),
app_state.clone(),
cx,
)
},
) )
.1 .1
}); });
@ -908,18 +897,24 @@ impl Workspace {
} }
}); });
} else if T::should_zoom_in_on_event(event) { } else if T::should_zoom_in_on_event(event) {
this.zoom_out(cx);
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
if panel.has_focus(cx) { if panel.has_focus(cx) {
this.zoomed = Some(panel.downgrade().into_any()); this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx)); this.zoomed_position = Some(panel.read(cx).position(cx));
} }
} else if T::should_zoom_out_on_event(event) { } else if T::should_zoom_out_on_event(event) {
this.zoom_out(cx); dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
if this.zoomed_position == Some(prev_position) {
this.zoomed = None;
this.zoomed_position = None;
}
cx.notify();
} else if T::is_focus_event(event) { } else if T::is_focus_event(event) {
let position = panel.read(cx).position(cx);
this.dismiss_zoomed_items_to_reveal(Some(position), cx);
if panel.is_zoomed(cx) { if panel.is_zoomed(cx) {
this.zoomed = Some(panel.downgrade().into_any()); this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx)); this.zoomed_position = Some(position);
} else { } else {
this.zoomed = None; this.zoomed = None;
this.zoomed_position = None; this.zoomed_position = None;
@ -968,9 +963,8 @@ impl Workspace {
let timestamp = entry.timestamp; let timestamp = entry.timestamp;
match history.entry(project_path) { match history.entry(project_path) {
hash_map::Entry::Occupied(mut entry) => { hash_map::Entry::Occupied(mut entry) => {
let (old_fs_path, old_timestamp) = entry.get(); let (_, old_timestamp) = entry.get();
if &timestamp > old_timestamp { if &timestamp > old_timestamp {
assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
entry.insert((fs_path, timestamp)); entry.insert((fs_path, timestamp));
} }
} }
@ -1592,7 +1586,7 @@ impl Workspace {
DockPosition::Right => &self.right_dock, DockPosition::Right => &self.right_dock,
}; };
let mut focus_center = false; let mut focus_center = false;
let mut zoom_out = false; let mut reveal_dock = false;
dock.update(cx, |dock, cx| { dock.update(cx, |dock, cx| {
let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
let was_visible = dock.is_open() && !other_is_zoomed; let was_visible = dock.is_open() && !other_is_zoomed;
@ -1607,14 +1601,15 @@ impl Workspace {
if active_panel.is_zoomed(cx) { if active_panel.is_zoomed(cx) {
cx.focus(active_panel.as_any()); cx.focus(active_panel.as_any());
} }
zoom_out = true; reveal_dock = true;
} }
} }
}); });
if zoom_out { if reveal_dock {
self.zoom_out_everything_except(dock_side, cx); self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
} }
if focus_center { if focus_center {
cx.focus_self(); cx.focus_self();
} }
@ -1623,62 +1618,49 @@ impl Workspace {
self.serialize_workspace(cx); self.serialize_workspace(cx);
} }
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> { pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
self.show_or_hide_panel::<T>(cx, |_, _| true)? self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
.as_any() .as_any()
.clone() .clone()
.downcast() .downcast()
} }
/// Focus the panel of the given type if it isn't already focused. If it is
/// already focused, then transfer focus back to the workspace center.
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) { pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
self.show_or_hide_panel::<T>(cx, |panel, cx| !panel.has_focus(cx)); self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
} }
fn show_or_hide_panel<T: Panel>( /// Focus or unfocus the given panel type, depending on the given callback.
fn focus_or_unfocus_panel<T: Panel>(
&mut self, &mut self,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
show: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool, should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
) -> Option<Rc<dyn PanelHandle>> { ) -> Option<Rc<dyn PanelHandle>> {
for (dock, position) in [ for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
self.left_dock.clone(),
self.bottom_dock.clone(),
self.right_dock.clone(),
]
.into_iter()
.zip(
[
DockPosition::Left,
DockPosition::Bottom,
DockPosition::Right,
]
.into_iter(),
) {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() { if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
let mut focus_center = false; let mut focus_center = false;
let mut zoom_out = false; let mut reveal_dock = false;
let panel = dock.update(cx, |dock, cx| { let panel = dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx); dock.activate_panel(panel_index, cx);
let panel = dock.active_panel().cloned(); let panel = dock.active_panel().cloned();
if let Some(panel) = panel.as_ref() { if let Some(panel) = panel.as_ref() {
let should_show = show(&**panel, cx); if should_focus(&**panel, cx) {
if should_show {
dock.set_open(true, cx); dock.set_open(true, cx);
cx.focus(panel.as_any()); cx.focus(panel.as_any());
zoom_out = true; reveal_dock = true;
} else { } else {
if panel.is_zoomed(cx) { // if panel.is_zoomed(cx) {
dock.set_open(false, cx); // dock.set_open(false, cx);
} // }
focus_center = true; focus_center = true;
} }
} }
panel panel
}); });
if zoom_out {
self.zoom_out_everything_except(position, cx);
}
if focus_center { if focus_center {
cx.focus_self(); cx.focus_self();
} }
@ -1691,6 +1673,16 @@ impl Workspace {
None None
} }
pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
let dock = dock.read(cx);
if let Some(panel) = dock.panel::<T>() {
return Some(panel);
}
}
None
}
fn zoom_out(&mut self, cx: &mut ViewContext<Self>) { fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
for pane in &self.panes { for pane in &self.panes {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
@ -1705,28 +1697,38 @@ impl Workspace {
cx.notify(); cx.notify();
} }
fn zoom_out_everything_except( fn dismiss_zoomed_items_to_reveal(
&mut self, &mut self,
except_position: DockPosition, dock_to_reveal: Option<DockPosition>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
// If a center pane is zoomed, unzoom it.
for pane in &self.panes { for pane in &self.panes {
if pane != &self.active_pane || dock_to_reveal.is_some() {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
} }
if except_position != DockPosition::Left {
self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
} }
if except_position != DockPosition::Bottom { // If another dock is zoomed, hide it.
self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); let mut focus_center = false;
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
dock.update(cx, |dock, cx| {
if Some(dock.position()) != dock_to_reveal {
if let Some(panel) = dock.active_panel() {
if panel.is_zoomed(cx) {
focus_center |= panel.has_focus(cx);
dock.set_open(false, cx);
}
}
}
});
} }
if except_position != DockPosition::Right { if focus_center {
self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); cx.focus_self();
} }
if self.zoomed_position != Some(except_position) { if self.zoomed_position != dock_to_reveal {
self.zoomed = None; self.zoomed = None;
self.zoomed_position = None; self.zoomed_position = None;
} }
@ -1937,6 +1939,7 @@ impl Workspace {
self.last_active_center_pane = Some(pane.downgrade()); self.last_active_center_pane = Some(pane.downgrade());
} }
self.dismiss_zoomed_items_to_reveal(None, cx);
if pane.read(cx).is_zoomed() { if pane.read(cx).is_zoomed() {
self.zoomed = Some(pane.downgrade().into_any()); self.zoomed = Some(pane.downgrade().into_any());
} else { } else {
@ -1998,7 +2001,6 @@ impl Workspace {
} }
pane::Event::ZoomIn => { pane::Event::ZoomIn => {
if pane == self.active_pane { if pane == self.active_pane {
self.zoom_out(cx);
pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
if pane.read(cx).has_focus() { if pane.read(cx).has_focus() {
self.zoomed = Some(pane.downgrade().into_any()); self.zoomed = Some(pane.downgrade().into_any());
@ -2007,7 +2009,13 @@ impl Workspace {
cx.notify(); cx.notify();
} }
} }
pane::Event::ZoomOut => self.zoom_out(cx), pane::Event::ZoomOut => {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
if self.zoomed_position.is_none() {
self.zoomed = None;
}
cx.notify();
}
} }
self.serialize_workspace(cx); self.serialize_workspace(cx);
@ -3101,6 +3109,17 @@ impl Workspace {
} }
} }
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
ZED_WINDOW_POSITION
.zip(*ZED_WINDOW_SIZE)
.map(|(position, size)| {
WindowBounds::Fixed(RectF::new(
cx.platform().screens()[0].bounds().origin() + position,
size,
))
})
}
async fn open_items( async fn open_items(
serialized_workspace: Option<SerializedWorkspace>, serialized_workspace: Option<SerializedWorkspace>,
workspace: &WeakViewHandle<Workspace>, workspace: &WeakViewHandle<Workspace>,
@ -3190,6 +3209,87 @@ async fn open_items(
opened_items opened_items
} }
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
const MESSAGE_ID: usize = 2;
if workspace
.read_with(cx, |workspace, cx| {
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
})
.unwrap_or(false)
{
return;
}
if db::kvp::KEY_VALUE_STORE
.read_kvp(NEW_DOCK_HINT_KEY)
.ok()
.flatten()
.is_some()
{
if !workspace
.read_with(cx, |workspace, cx| {
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
})
.unwrap_or(false)
{
cx.update(|cx| {
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
let entry = tracker
.entry(TypeId::of::<MessageNotification>())
.or_default();
if !entry.contains(&MESSAGE_ID) {
entry.push(MESSAGE_ID);
}
});
});
}
return;
}
cx.spawn(|_| async move {
db::kvp::KEY_VALUE_STORE
.write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
.await
.ok();
})
.detach();
workspace
.update(cx, |workspace, cx| {
workspace.show_notification_once(2, cx, |cx| {
cx.add_view(|_| {
MessageNotification::new_element(|text, _| {
Text::new(
"Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
text,
)
.with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
let code_span_background_color = settings::get::<ThemeSettings>(cx)
.theme
.editor
.document_highlight_read_background;
scene.push_quad(gpui::Quad {
bounds,
background: Some(code_span_background_color),
border: Default::default(),
corner_radius: 2.0,
})
})
.into_any()
})
.with_click_message("Read more about the new panel system")
.on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
})
})
})
.ok();
}
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) { fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml"; const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
@ -3206,7 +3306,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
} else { } else {
let backup_path = (*db::BACKUP_DB_PATH).read(); let backup_path = (*db::BACKUP_DB_PATH).read();
if let Some(backup_path) = backup_path.clone() { if let Some(backup_path) = backup_path.clone() {
workspace.show_notification_once(0, cx, move |cx| { workspace.show_notification_once(1, cx, move |cx| {
cx.add_view(move |_| { cx.add_view(move |_| {
MessageNotification::new(format!( MessageNotification::new(format!(
"Database file was corrupted. Old database backed up to {}", "Database file was corrupted. Old database backed up to {}",
@ -3278,32 +3378,36 @@ impl View for Workspace {
enum ZoomBackground {} enum ZoomBackground {}
let zoomed = zoomed.upgrade(cx)?; let zoomed = zoomed.upgrade(cx)?;
let mut foreground_style; let mut foreground_style =
match self.zoomed_position {
Some(DockPosition::Left) => {
foreground_style =
theme.workspace.zoomed_panel_foreground;
foreground_style.margin.left = 0.;
foreground_style.margin.top = 0.;
foreground_style.margin.bottom = 0.;
}
Some(DockPosition::Right) => {
foreground_style =
theme.workspace.zoomed_panel_foreground;
foreground_style.margin.right = 0.;
foreground_style.margin.top = 0.;
foreground_style.margin.bottom = 0.;
}
Some(DockPosition::Bottom) => {
foreground_style =
theme.workspace.zoomed_panel_foreground;
foreground_style.margin.left = 0.;
foreground_style.margin.right = 0.;
foreground_style.margin.bottom = 0.;
}
None => {
foreground_style =
theme.workspace.zoomed_pane_foreground; theme.workspace.zoomed_pane_foreground;
if let Some(zoomed_dock_position) = self.zoomed_position {
foreground_style =
theme.workspace.zoomed_panel_foreground;
let margin = foreground_style.margin.top;
let border = foreground_style.border.top;
// Only include a margin and border on the opposite side.
foreground_style.margin.top = 0.;
foreground_style.margin.left = 0.;
foreground_style.margin.bottom = 0.;
foreground_style.margin.right = 0.;
foreground_style.border.top = false;
foreground_style.border.left = false;
foreground_style.border.bottom = false;
foreground_style.border.right = false;
match zoomed_dock_position {
DockPosition::Left => {
foreground_style.margin.right = margin;
foreground_style.border.right = border;
}
DockPosition::Right => {
foreground_style.margin.left = margin;
foreground_style.border.left = border;
}
DockPosition::Bottom => {
foreground_style.margin.top = margin;
foreground_style.border.top = border;
}
} }
} }
@ -3548,8 +3652,13 @@ pub fn join_remote_project(
}) })
.await?; .await?;
let window_bounds_override = window_bounds_env_override(&cx);
let (_, workspace) = cx.add_window( let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()), (app_state.build_window_options)(
window_bounds_override,
None,
cx.platform().as_ref(),
),
|cx| Workspace::new(0, project, app_state.clone(), cx), |cx| Workspace::new(0, project, app_state.clone(), cx),
); );
(app_state.initialize_workspace)( (app_state.initialize_workspace)(
@ -4257,6 +4366,12 @@ mod tests {
panel panel
}); });
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
let item = cx.add_view(|_| TestItem::new());
pane.add_item(Box::new(item), true, true, None, cx);
});
// Transfer focus from center to panel // Transfer focus from center to panel
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx); workspace.toggle_panel_focus::<TestPanel>(cx);
@ -4324,7 +4439,7 @@ mod tests {
assert!(!panel.has_focus(cx)); assert!(!panel.has_focus(cx));
}); });
// Transfering focus back to the panel keeps it zoomed // Transferring focus back to the panel keeps it zoomed
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx); workspace.toggle_panel_focus::<TestPanel>(cx);
}); });
@ -4358,6 +4473,25 @@ mod tests {
assert!(workspace.zoomed.is_some()); assert!(workspace.zoomed.is_some());
assert!(panel.has_focus(cx)); assert!(panel.has_focus(cx));
}); });
// Unzoom and close the panel, zoom the active pane.
panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
// Opening a dock unzooms the pane.
workspace.update(cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Right, cx)
});
workspace.read_with(cx, |workspace, cx| {
let pane = pane.read(cx);
assert!(!pane.is_zoomed());
assert!(pane.has_focus());
assert!(workspace.right_dock().read(cx).is_open());
assert!(workspace.zoomed.is_none());
});
} }
#[gpui::test] #[gpui::test]

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