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 }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
steps:
- name: Install Rust
run: |

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.65-bullseye as builder
FROM rust:1.70-bullseye as builder
WORKDIR app
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">
<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"/>
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>

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
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",

View file

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

View file

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

View file

@ -81,6 +81,14 @@
// Default width of the project panel.
"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.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
@ -245,7 +253,7 @@
// copy to the system clipboard.
"copy_on_select": false,
// 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": {
// "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
// documentation: https://zed.dev/docs/configuring-zed
// For a full list of overridable settings, and general information on folder-specific settings, see the documentation:
// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
//
// To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command

View file

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

View file

@ -1,22 +1,10 @@
use anyhow::{anyhow, Result};
use assets::Assets;
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};
pub mod assistant;
mod assistant_settings;
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
#[derive(Serialize)]
@ -38,7 +26,7 @@ struct ResponseMessage {
content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Role {
User,
@ -46,6 +34,26 @@ enum Role {
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)]
struct OpenAIResponseStreamEvent {
pub id: Option<String>,
@ -86,228 +94,5 @@ struct OpenAIChoice {
}
pub fn init(cx: &mut AppContext) {
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
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,
))
}
assistant::init(cx);
}

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 {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath {
executable: excutable,
..
} => excutable,
Self::LocalPath { executable, .. } => executable,
}
}

View file

@ -776,15 +776,6 @@ impl Client {
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx);
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() {
let mut status_rx = self.status();
@ -1072,11 +1063,8 @@ impl Client {
) -> Task<Result<Credentials>> {
let platform = cx.platform();
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
executor.clone().spawn(async move {
// 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
@ -1159,12 +1147,6 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
telemetry.report_mixpanel_event(
"authenticate with browser",
Default::default(),
telemetry_settings,
);
Ok(Credentials {
user_id: user_id.parse()?,
access_token,

View file

@ -1,14 +1,9 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use gpui::{executor::Background, serde_json, AppContext, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::{
env,
io::Write,
@ -19,7 +14,7 @@ use std::{
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use util::{channel::ReleaseChannel, TryFutureExt};
use uuid::Uuid;
pub struct Telemetry {
@ -37,23 +32,15 @@ struct TelemetryState {
os_name: &'static str,
os_version: Option<Arc<str>>,
architecture: &'static str,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
flush_mixpanel_events_task: Option<Task<()>>,
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
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";
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 =
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)]
const MAX_QUEUE_LEN: usize = 1;
@ -168,29 +114,13 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
mixpanel_events_queue: Default::default(),
clickhouse_events_queue: Default::default(),
flush_mixpanel_events_task: Default::default(),
flush_clickhouse_events_task: Default::default(),
next_mixpanel_event_id: 0,
log_file: 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
}
@ -218,20 +148,9 @@ impl Telemetry {
let mut state = this.state.lock();
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();
drop(state);
if has_mixpanel_events {
this.flush_mixpanel_events();
}
drop(state);
if has_clickhouse_events {
this.flush_clickhouse_events();
@ -256,37 +175,11 @@ impl Telemetry {
return;
}
let this = self.clone();
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());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
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(
@ -310,7 +203,7 @@ impl Telemetry {
});
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);
self.flush_clickhouse_events();
} 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>> {
self.state.lock().metrics_id.clone()
}
@ -385,44 +229,6 @@ impl Telemetry {
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>) {
let mut state = self.state.lock();
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"
edition = "2021"
name = "collab"
version = "0.12.4"
version = "0.13.1"
publish = false
[[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_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" (
"project_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_repository;
mod worktree_repository_statuses;
mod worktree_settings_file;
use crate::executor::Executor;
use crate::{Error, Result};
@ -1494,6 +1495,7 @@ impl Database {
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
@ -1591,6 +1593,25 @@ impl Database {
})
.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
.find_related(project_collaborator::Entity)
.all(&*tx)
@ -2530,6 +2551,58 @@ impl Database {
.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(
&self,
project_id: ProjectId,
@ -2600,6 +2673,7 @@ impl Database {
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.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.
let language_servers = project
.find_related(language_server::Entity)
@ -3347,6 +3440,7 @@ pub struct RejoinedWorktree {
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
@ -3402,10 +3496,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
}
#[cfg(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(update_language_server)
.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::GetDefinition>)
.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 {
@ -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 {
@ -1525,6 +1550,29 @@ async fn update_diagnostic_summary(
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(
request: proto::StartLanguageServer,
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)]
async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>,

View file

@ -472,7 +472,7 @@ impl CollabTitlebarItem {
Stack::new()
.with_child(
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);
Label::new(label, style.text.clone())
.contained()

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ pub mod items;
mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod multi_buffer;
pub mod multi_buffer;
mod persistence;
pub mod scroll;
pub mod selections_collection;
@ -31,9 +31,13 @@ use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
pub use editor_settings::EditorSettings;
pub use element::*;
pub use element::RenderExcerptHeaderParams;
pub use element::{
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
};
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::LayoutContext;
use gpui::{
actions,
color::Color,
@ -44,9 +48,9 @@ use gpui::{
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
serde_json::{self, json},
AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -109,6 +113,12 @@ pub struct SelectNext {
pub replace_newest: bool,
}
#[derive(Clone, Deserialize, PartialEq, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
@ -270,6 +280,7 @@ impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectToBeginningOfLine,
SelectToEndOfLine,
ToggleCodeActions,
@ -365,6 +376,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::add_selection_above);
cx.add_action(Editor::add_selection_below);
cx.add_action(Editor::select_next);
cx.add_action(Editor::select_previous);
cx.add_action(Editor::toggle_comments);
cx.add_action(Editor::select_larger_syntax_node);
cx.add_action(Editor::select_smaller_syntax_node);
@ -482,6 +494,7 @@ pub struct Editor {
columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
selection_history: SelectionHistory,
autoclose_regions: Vec<AutocloseRegion>,
snippet_stack: InvalidationStack<SnippetState>,
@ -496,7 +509,9 @@ pub struct Editor {
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
placeholder_text: Option<Arc<str>>,
render_excerpt_header: Option<element::RenderExcerptHeader>,
highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)]
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
@ -526,6 +541,7 @@ pub struct Editor {
pub struct EditorSnapshot {
pub mode: EditorMode,
pub show_gutter: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
@ -537,6 +553,7 @@ pub struct EditorSnapshot {
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
add_selections_state: Option<AddSelectionsState>,
}
@ -1284,6 +1301,7 @@ impl Editor {
columnar_selection_tail: None,
add_selections_state: None,
select_next_state: None,
select_prev_state: None,
selection_history: Default::default(),
autoclose_regions: Default::default(),
snippet_stack: Default::default(),
@ -1297,7 +1315,9 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
show_gutter: mode == EditorMode::Full,
placeholder_text: None,
render_excerpt_header: None,
highlighted_rows: None,
background_highlights: Default::default(),
nav_history: None,
@ -1393,6 +1413,7 @@ impl Editor {
pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
@ -1505,6 +1526,7 @@ impl Editor {
let buffer = &display_map.buffer_snapshot;
self.add_selections_state = None;
self.select_next_state = None;
self.select_prev_state = None;
self.select_larger_syntax_node_stack.clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
self.snippet_stack
@ -2157,40 +2179,72 @@ impl Editor {
indent.len = cmp::min(indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let is_cursor = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) =
if let Some(language) = &language_scope {
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let mut insert_extra_newline = false;
if let Some(language) = buffer.language_scope_at(start) {
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let trailing_whitespace_len = buffer
.chars_at(end)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let trailing_whitespace_len = buffer
.chars_at(end)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let insert_extra_newline =
language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
enabled
&& pair.newline
&& buffer.contains_str_at(
end + trailing_whitespace_len,
pair_end,
)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
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)
};
enabled
&& pair.newline
&& buffer
.contains_str_at(end + trailing_whitespace_len, pair_end)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
pair_start,
)
});
}
let mut new_text = String::with_capacity(1 + indent.len as usize);
new_text.push('\n');
let capacity_for_delimiter = comment_delimiter
.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());
if let Some(delimiter) = &comment_delimiter {
new_text.push_str(&delimiter);
}
if insert_extra_newline {
new_text = new_text.repeat(2);
}
@ -2372,7 +2426,7 @@ impl Editor {
old_selections
.iter()
.map(|s| {
let anchor = snapshot.anchor_after(s.end);
let anchor = snapshot.anchor_after(s.head());
s.map(|_| anchor)
})
.collect::<Vec<_>>()
@ -2525,7 +2579,7 @@ impl Editor {
.read(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.
let push_to_lsp_host_history = true;
// If this is not the host, append its history with new edits.
@ -3207,12 +3261,10 @@ impl Editor {
snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>,
) -> bool {
let path = snapshot.file_at(location).map(|file| file.path().as_ref());
let language_name = snapshot
.language_at(location)
.map(|language| language.name());
let settings = all_language_settings(cx);
settings.copilot_enabled(language_name.as_deref(), path)
let file = snapshot.file_at(location);
let language = snapshot.language_at(location);
let settings = all_language_settings(file, cx);
settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@ -3549,7 +3601,9 @@ impl Editor {
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
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>) {
self.transact(cx, |this, 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() {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
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.request_autoscroll(Autoscroll::newest(), cx);
}
@ -5598,6 +5748,7 @@ impl Editor {
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.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
@ -6375,6 +6526,7 @@ impl Editor {
self.selection_history.push(SelectionHistoryEntry {
selections: self.selections.disjoint_anchors(),
select_next_state: self.select_next_state.clone(),
select_prev_state: self.select_prev_state.clone(),
add_selections_state: self.add_selections_state.clone(),
});
}
@ -6654,6 +6806,25 @@ impl Editor {
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>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@ -6878,7 +7049,7 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => {
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
let file_extension = file_extension.or(self
let file = self
.buffer
.read(cx)
.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(|e| e.to_str())
.map(|a| a.to_string()));
@ -7091,7 +7264,7 @@ impl Editor {
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
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
.buffer
.read(cx)
@ -7099,15 +7272,6 @@ impl Editor {
.show_copilot_suggestions;
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 {
file_extension,
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()
.with_child(EditorElement::new(style.clone()))
.with_child(editor)
.with_child(ChildView::new(&self.mouse_context_menu, cx))
.into_any()
}
@ -7801,13 +7969,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
}
pub fn highlight_diagnostic_message(
inital_highlights: Vec<usize>,
initial_highlights: Vec<usize>,
message: &str,
) -> (String, Vec<usize>) {
let mut message_without_backticks = String::new();
let mut prev_offset = 0;
let mut inside_block = false;
let mut highlights = inital_highlights;
let mut highlights = initial_highlights;
for (match_ix, (offset, _)) in message
.match_indices('`')
.chain([(message.len(), "")])

View file

@ -9,7 +9,8 @@ use gpui::{
executor::Deterministic,
geometry::{rect::RectF, vector::vec2f},
platform::{WindowBounds, WindowOptions},
serde_json, TestAppContext,
serde_json::{self, json},
TestAppContext,
};
use indoc::indoc;
use language::{
@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
// 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);
let invalid_point = Point::new(9999, 0);
editor.navigate(
@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
cursor_anchor: invalid_anchor,
cursor_position: invalid_point,
scroll_anchor: ScrollAnchor {
top_anchor: invalid_anchor,
anchor: invalid_anchor,
offset: Default::default(),
},
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]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
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ˇ»");
}
#[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]
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
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)));
// 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| {
settings.languages.insert(
"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)));
// 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| {
settings.languages.insert(
"Rust".into(),
@ -4725,7 +4804,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
two
threeˇ
"},
"overlapping aditional edit",
"overlapping additional edit",
),
(
indoc! {"
@ -5225,7 +5304,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
Point::new(0, 1)..Point::new(0, 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);
follower.set_scroll_anchor(
ScrollAnchor {
top_anchor,
anchor: top_anchor,
offset: vec2f(0.0, 0.5),
},
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 {
style: Arc<EditorStyle>,
render_excerpt_header: RenderExcerptHeader,
}
impl EditorElement {
pub fn new(style: EditorStyle) -> Self {
Self {
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(
scene: &mut SceneBuilder,
position_map: &Arc<PositionMap>,
@ -1465,11 +1488,9 @@ impl EditorElement {
line_height: f32,
style: &EditorStyle,
line_layouts: &[LineWithInvisibles],
include_root: bool,
editor: &mut Editor,
cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) {
let tooltip_style = theme::current(cx).tooltip.clone();
let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone())
@ -1510,112 +1531,18 @@ impl EditorElement {
range,
starts_new_buffer,
..
} => {
let id = *id;
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 = 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 = &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")
}
}
} => (self.render_excerpt_header)(
editor,
RenderExcerptHeaderParams {
id: *id,
buffer,
range,
starts_new_buffer: *starts_new_buffer,
gutter_padding,
editor_style: style,
},
cx,
),
};
element.layout(
@ -1899,7 +1826,7 @@ impl Element<Editor> for EditorElement {
let gutter_padding;
let gutter_width;
let gutter_margin;
if snapshot.mode == EditorMode::Full {
if snapshot.show_gutter {
let em_width = style.text.em_width(cx.font_cache());
gutter_padding = (em_width * style.gutter_padding_factor).round();
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,
};
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
.into_iter()
.map(|(id, fold)| {
@ -2144,7 +2065,6 @@ impl Element<Editor> for EditorElement {
line_height,
&style,
&line_layouts,
include_root,
editor,
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(
position: Vector2F,
text_bounds: RectF,
@ -3080,7 +3115,7 @@ mod tests {
editor_width: f32,
) -> Vec<Invisible> {
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 buffer = MultiBuffer::build_simple(&input_text, cx);

View file

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

View file

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

View file

@ -64,6 +64,9 @@ pub enum Event {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
Edited,
Reloaded,
DiffBaseChanged,
@ -196,6 +199,13 @@ pub struct MultiBufferBytes<'a> {
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> {
content_chunks: BufferChunks<'a>,
footer_height: usize,
@ -387,6 +397,7 @@ impl MultiBuffer {
original_indent_column: u32,
}
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
let mut edited_excerpt_ids = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into();
@ -403,6 +414,7 @@ impl MultiBuffer {
.start
.to_offset(&start_excerpt.buffer)
+ start_overshoot;
edited_excerpt_ids.push(start_excerpt.id);
cursor.seek(&range.end, Bias::Right, &());
if cursor.item().is_none() && range.end == *cursor.start() {
@ -428,6 +440,7 @@ impl MultiBuffer {
original_indent_column,
});
} else {
edited_excerpt_ids.push(end_excerpt.id);
let start_excerpt_range = buffer_start
..start_excerpt
.range
@ -474,6 +487,7 @@ impl MultiBuffer {
is_insertion: false,
original_indent_column,
});
edited_excerpt_ids.push(excerpt.id);
cursor.next(&());
}
}
@ -546,6 +560,10 @@ impl MultiBuffer {
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> {
@ -1377,8 +1395,14 @@ impl MultiBuffer {
point: T,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(point, cx);
language_settings(language.map(|l| l.name()).as_deref(), cx)
let mut language = None;
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>)) {
@ -1961,7 +1985,6 @@ impl MultiBufferSnapshot {
} else {
None
};
MultiBufferBytes {
range,
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 {
let mut result = MultiBufferRows {
buffer_row_range: 0..0,
@ -2785,9 +2835,13 @@ impl MultiBufferSnapshot {
point: T,
cx: &'a AppContext,
) -> &'a LanguageSettings {
self.point_to_buffer_offset(point)
.map(|(buffer, offset)| buffer.settings_at(offset, cx))
.unwrap_or_else(|| language_settings(None, cx))
let mut language = None;
let mut file = None;
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> {
@ -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 {
if text_anchor
.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> {
type Item = &'a [u8];
@ -5237,7 +5343,7 @@ mod tests {
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
// 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.
multibuffer.undo(cx);
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");

View file

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

View file

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

View file

@ -76,6 +76,9 @@ impl SelectionsCollection {
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>]> {
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>) {
let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), umarked_text);
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
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 util::channel::ReleaseChannel;
// TODO: Move this file out of feedback and into a more general place
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
#[serde(serialize_with = "serialize_app_version")]

View file

@ -33,7 +33,7 @@ use repository::{FakeGitRepositoryState, GitFileStatus};
use std::sync::Weak;
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)]
@ -78,13 +78,13 @@ impl LineEnding {
}
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;
}
}
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()
} else {
text

View file

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

View file

@ -6335,9 +6335,9 @@ mod tests {
#[crate::test(self)]
async fn test_labeled_tasks(cx: &mut TestAppContext) {
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
.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!(
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> {
let root_view_id = self.window.root_view().id();
let focused_view_id = self.window.focused_view_id?;
self.window
.rendered_views
.get(&root_view_id)?
.get(&focused_view_id)?
.rect_for_text_range(range_utf16, self)
.log_err()
.flatten()

View file

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

View file

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

View file

@ -67,7 +67,7 @@ impl KeymapMatcher {
/// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys.
/// 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,
// and order in the keymap second.
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.
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 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 {
let language_name = self.language_at(position).map(|language| language.name());
let settings = language_settings(language_name.as_deref(), cx);
let settings = language_settings(self.language_at(position), self.file(), cx);
if settings.hard_tabs {
IndentSize::tab()
} else {
@ -2127,8 +2131,7 @@ impl BufferSnapshot {
position: D,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(position);
language_settings(language.map(|l| l.name()).as_deref(), cx)
language_settings(self.language_at(position), self.file.as_ref(), cx)
}
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>> {
self.outline_items_containing(0..self.len(), theme)
self.outline_items_containing(0..self.len(), true, theme)
.map(Outline::new)
}
@ -2262,6 +2265,7 @@ impl BufferSnapshot {
let position = position.to_offset(self);
let mut items = self.outline_items_containing(
position.saturating_sub(1)..self.len().min(position + 1),
false,
theme,
)?;
let mut prev_depth = None;
@ -2276,6 +2280,7 @@ impl BufferSnapshot {
fn outline_items_containing(
&self,
range: Range<usize>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> {
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
@ -2310,7 +2315,10 @@ impl BufferSnapshot {
let node_is_name;
if capture.index == config.name_capture_ix {
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;
} else {
continue;
@ -2337,10 +2345,12 @@ impl BufferSnapshot {
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true,
);
let mut last_buffer_range_end = 0;
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(' ');
}
last_buffer_range_end = buffer_range.end;
if is_name {
let mut start = text.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]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"

View file

@ -34,7 +34,7 @@ use std::{
fmt::Debug,
hash::Hash,
mem,
ops::Range,
ops::{Not, Range},
path::{Path, PathBuf},
str,
sync::{
@ -455,6 +455,7 @@ struct OutlineConfig {
item_capture_ix: u32,
name_capture_ix: u32,
context_capture_ix: Option<u32>,
extra_context_capture_ix: Option<u32>,
}
struct InjectionConfig {
@ -500,6 +501,7 @@ struct AvailableLanguage {
grammar: tree_sitter::Language,
lsp_adapters: Vec<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
loaded: bool,
}
pub struct LanguageRegistry {
@ -527,6 +529,7 @@ struct LanguageRegistryState {
subscription: (watch::Sender<()>, watch::Receiver<()>),
theme: Option<Arc<Theme>>,
version: usize,
reload_count: usize,
}
pub struct PendingLanguageServer {
@ -547,6 +550,7 @@ impl LanguageRegistry {
subscription: watch::channel(),
theme: Default::default(),
version: 0,
reload_count: 0,
}),
language_server_download_dir: None,
lsp_binary_statuses_tx,
@ -566,6 +570,14 @@ impl LanguageRegistry {
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(
&self,
path: &'static str,
@ -582,6 +594,7 @@ impl LanguageRegistry {
grammar,
lsp_adapters,
get_queries,
loaded: false,
});
}
@ -590,7 +603,7 @@ impl LanguageRegistry {
let mut result = state
.available_languages
.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()))
.collect::<Vec<_>>();
result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
@ -603,6 +616,7 @@ impl LanguageRegistry {
state
.available_languages
.iter()
.filter(|l| !l.loaded)
.flat_map(|l| l.lsp_adapters.clone())
.chain(
state
@ -639,10 +653,17 @@ impl LanguageRegistry {
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 {
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>) {
let mut state = self.state.write();
state.theme = Some(theme.clone());
@ -721,7 +742,7 @@ impl LanguageRegistry {
if let Some(language) = state
.available_languages
.iter()
.find(|l| callback(&l.config))
.find(|l| !l.loaded && callback(&l.config))
.cloned()
{
let txs = state
@ -743,9 +764,7 @@ impl LanguageRegistry {
let language = Arc::new(language);
let mut state = this.state.write();
state.add(language.clone());
state
.available_languages
.retain(|language| language.id != id);
state.mark_language_loaded(id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Ok(language.clone()));
@ -753,10 +772,9 @@ impl LanguageRegistry {
}
}
Err(err) => {
log::error!("failed to load language {name} - {err}");
let mut state = this.state.write();
state
.available_languages
.retain(|language| language.id != id);
state.mark_language_loaded(id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Err(anyhow!(
@ -905,6 +923,28 @@ impl LanguageRegistryState {
self.version += 1;
*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"))]
@ -1021,34 +1061,22 @@ impl Language {
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
.with_highlights_query(query.as_ref())
.expect("failed to evaluate highlights query");
self = self.with_highlights_query(query.as_ref())?;
}
if let Some(query) = queries.brackets {
self = self
.with_brackets_query(query.as_ref())
.expect("failed to load brackets query");
self = self.with_brackets_query(query.as_ref())?;
}
if let Some(query) = queries.indents {
self = self
.with_indents_query(query.as_ref())
.expect("failed to load indents query");
self = self.with_indents_query(query.as_ref())?;
}
if let Some(query) = queries.outline {
self = self
.with_outline_query(query.as_ref())
.expect("failed to load outline query");
self = self.with_outline_query(query.as_ref())?;
}
if let Some(query) = queries.injections {
self = self
.with_injection_query(query.as_ref())
.expect("failed to load injection query");
self = self.with_injection_query(query.as_ref())?;
}
if let Some(query) = queries.overrides {
self = self
.with_override_query(query.as_ref())
.expect("failed to load override query");
self = self.with_override_query(query.as_ref())?;
}
Ok(self)
}
@ -1064,12 +1092,14 @@ impl Language {
let mut item_capture_ix = None;
let mut name_capture_ix = None;
let mut context_capture_ix = None;
let mut extra_context_capture_ix = None;
get_capture_indices(
&query,
&mut [
("item", &mut item_capture_ix),
("name", &mut name_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) {
@ -1078,6 +1108,7 @@ impl Language {
item_capture_ix,
name_capture_ix,
context_capture_ix,
extra_context_capture_ix,
});
}
Ok(self)

View file

@ -1,3 +1,4 @@
use crate::{File, Language};
use anyhow::Result;
use collections::HashMap;
use globset::GlobMatcher;
@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
settings::register::<AllLanguageSettings>(cx);
}
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
settings::get::<AllLanguageSettings>(cx).language(language)
pub fn language_settings<'a>(
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 {
settings::get::<AllLanguageSettings>(cx)
pub fn all_language_settings<'a>(
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)]
@ -155,7 +165,7 @@ impl AllLanguageSettings {
.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 {
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>();
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
// settings with specific languages.
assert!(root_schema
.definitions
.contains_key("LanguageSettingsContent"));

View file

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

View file

@ -260,9 +260,10 @@ impl LanguageServer {
buffer.clear();
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)
.ok_or_else(|| anyhow!("invalid header"))?
.ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
.trim_end()
.parse()?;
@ -301,7 +302,7 @@ impl LanguageServer {
}
} else {
warn!(
"Failed to deserialize message:\n{}",
"failed to deserialize LSP message:\n{}",
std::str::from_utf8(&buffer)?
);
}

View file

@ -18,4 +18,4 @@ metal = "0.21.0"
objc = "0.2"
[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()
/// }
/// ```
/// 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`.
#[proc_macro_attribute]
pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
/// #[import]
/// 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`.
/// All that's provided is a signature, as the function is implemented host-side.
#[proc_macro_attribute]

View file

@ -127,7 +127,7 @@ use plugin_handles::RopeHandle;
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:
@ -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.
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

View file

@ -132,7 +132,7 @@ impl PluginBuilder {
"env",
&format!("__{}", name),
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> = (|| {
// grab a handle to the memory
let plugin_memory = match caller.get_export("memory") {
@ -211,7 +211,7 @@ impl PluginBuilder {
"env",
&format!("__{}", name),
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> = (|| {
// grab a handle to the memory
let plugin_memory = match caller.get_export("memory") {
@ -297,7 +297,7 @@ pub enum PluginBinary<'a> {
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`].
pub struct Plugin {
store: Store<WasiCtxAlloc>,
@ -559,7 +559,7 @@ impl Plugin {
.ok_or_else(|| anyhow!("Could not grab slice of plugin 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(
self.store.data().alloc_buffer(),
&mut plugin_memory,
@ -569,7 +569,7 @@ impl Plugin {
.await?;
// 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
.function
.call_async(&mut self.store, arg_buffer.into_u64())

View file

@ -1111,14 +1111,18 @@ impl LspCommand for GetHover {
cx: AsyncAppContext,
) -> Result<Self::Response> {
Ok(message.and_then(|hover| {
let range = hover.range.map(|range| {
cx.read(|cx| {
let buffer = buffer.read(cx);
let token_start =
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);
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
})
let (language, range) = cx.read(|cx| {
let buffer = buffer.read(cx);
(
buffer.language().cloned(),
hover.range.map(|range| {
let token_start =
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);
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
}),
)
});
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,
message: proto::GetHoverResponse,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: AsyncAppContext,
buffer: ModelHandle<Buffer>,
cx: AsyncAppContext,
) -> 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
.contents
.into_iter()
@ -1271,12 +1272,23 @@ impl LspCommand for GetHover {
},
})
.collect();
if contents.is_empty() {
return Ok(None);
}
Ok(if contents.is_empty() {
None
let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
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 {
Some(Hover { contents, range })
})
None
};
Ok(Some(Hover {
contents,
range,
language,
}))
}
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
@ -1499,7 +1511,11 @@ impl LspCommand for GetCodeActions {
type ProtoRequest = proto::GetCodeActions;
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(
@ -1717,8 +1733,7 @@ impl LspCommand for OnTypeFormatting {
.await?;
let tab_size = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
language_settings(buffer.language(), buffer.file(), cx).tab_size
});
Ok(Self {

View file

@ -28,7 +28,7 @@ use gpui::{
ModelHandle, Task, WeakModelHandle,
};
use language::{
language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
language_settings::{language_settings, FormatOnSave, Formatter},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@ -72,7 +72,10 @@ use std::{
time::{Duration, Instant, SystemTime},
};
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 worktree::*;
@ -356,6 +359,7 @@ pub enum HoverBlockKind {
pub struct Hover {
pub contents: Vec<HoverBlock>,
pub range: Option<Range<language::Anchor>>,
pub language: Option<Arc<Language>>,
}
#[derive(Default)]
@ -460,6 +464,7 @@ impl Project {
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_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_rename_project_entry);
client.add_model_request_handler(Self::handle_copy_project_entry);
@ -519,7 +524,7 @@ impl Project {
_subscriptions: vec![
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),
active_entry: None,
languages,
@ -588,7 +593,7 @@ impl Project {
active_entry: None,
collaborators: Default::default(),
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),
languages,
user_store: user_store.clone(),
@ -686,42 +691,37 @@ impl Project {
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let settings = all_language_settings(cx);
let mut language_servers_to_start = Vec::new();
for buffer in self.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) {
let buffer = buffer.read(cx);
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
{
if settings
.language(Some(&language.name()))
.enable_language_server
{
let worktree = file.worktree.read(cx);
language_servers_to_start.push((
worktree.id(),
worktree.as_local().unwrap().abs_path().clone(),
language.clone(),
));
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
let settings = language_settings(Some(language), Some(file), cx);
if settings.enable_language_server {
if let Some(file) = File::from_dyn(Some(file)) {
language_servers_to_start
.push((file.worktree.clone(), language.clone()));
}
}
}
}
}
let mut language_servers_to_stop = Vec::new();
for language in 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() {
if lsp_name == started_lsp_name {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
}
}
let languages = self.languages.to_vec();
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
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()));
}
}
}
@ -733,8 +733,9 @@ impl Project {
}
// Start all the newly-enabled language servers.
for (worktree_id, worktree_path, language) in language_servers_to_start {
self.start_language_servers(worktree_id, worktree_path, language, cx);
for (worktree, language) in language_servers_to_start {
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() {
@ -1107,6 +1108,21 @@ impl Project {
.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 client = self.client.clone();
self.client_state = Some(ProjectClientState::Local {
@ -1219,6 +1235,14 @@ impl Project {
message_id: u32,
cx: &mut ModelContext<Self>,
) -> 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.set_worktrees_from_proto(message.worktrees, cx)?;
self.set_collaborators_from_proto(message.collaborators, cx)?;
@ -2215,13 +2239,34 @@ impl Project {
}
fn maintain_buffer_languages(
languages: &LanguageRegistry,
languages: Arc<LanguageRegistry>,
cx: &mut ModelContext<Project>,
) -> Task<()> {
let mut subscription = languages.subscribe();
let mut prev_reload_count = languages.reload_count();
cx.spawn_weak(|project, mut cx| async move {
while let Some(()) = subscription.next().await {
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| {
let mut plain_text_buffers = 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(worktree) = file.worktree.read(cx).as_local() {
let worktree_id = worktree.id();
let worktree_abs_path = worktree.abs_path().clone();
self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
let worktree = file.worktree.clone();
if let Some(tree) = worktree.read(cx).as_local() {
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
}
}
}
fn start_language_servers(
&mut self,
worktree_id: WorktreeId,
worktree: &ModelHandle<Worktree>,
worktree_path: Arc<Path>,
language: Arc<Language>,
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;
}
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() {
let key = (worktree_id, adapter.name.clone());
if self.language_server_ids.contains_key(&key) {
@ -2748,23 +2802,22 @@ impl Project {
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>,
) -> 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()
.filter_map(|buffer| {
let buffer = buffer.read(cx);
let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
let full_path = file.full_path(cx);
let language = self
.languages
.language_for_file(&full_path, Some(buffer.as_rope()))
.now_or_never()?
.ok()?;
Some((worktree.id(), worktree.abs_path().clone(), language))
Some((file.worktree.clone(), language))
})
.collect();
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
self.restart_language_servers(worktree_id, worktree_abs_path, language, cx);
for (worktree, language) in language_server_lookup_info {
self.restart_language_servers(worktree, language, cx);
}
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
fn restart_language_servers(
&mut self,
worktree_id: WorktreeId,
fallback_path: Arc<Path>,
worktree: ModelHandle<Worktree>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new();
for adapter in language.lsp_adapters() {
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()))
.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
for adapter in language.lsp_adapters() {
@ -3432,8 +3487,7 @@ impl Project {
let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
let settings = buffer.read_with(&cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
language_settings(language_name.as_deref(), cx).clone()
language_settings(buffer.language(), buffer.file(), cx).clone()
});
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()
&& 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
if !start_within && !end_within {
buffer.edit([(range, text)], None, cx);
@ -4463,11 +4517,14 @@ impl Project {
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
let tab_size = buffer.read_with(cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
let position = position.to_point_utf16(buffer);
(
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(
buffer.clone(),
OnTypeFormatting {
@ -4873,6 +4930,7 @@ impl Project {
worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
@ -4893,8 +4951,12 @@ impl Project {
.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);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.clear_local_settings(handle_id, cx).log_err()
});
})
.detach();
@ -5179,6 +5241,71 @@ impl Project {
.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>) {
let new_active_entry = entry.and_then(|project_path| {
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(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::CreateProjectEntry>,
@ -6521,8 +6672,8 @@ impl Project {
}
self.metadata_changed(cx);
for (id, _) in old_worktrees_by_id {
cx.emit(Event::WorktreeRemoved(id));
for id in old_worktrees_by_id.keys() {
cx.emit(Event::WorktreeRemoved(*id));
}
Ok(())
@ -6892,6 +7043,13 @@ impl WorktreeHandle {
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 {

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]
async fn test_managing_language_servers(
deterministic: Arc<Deterministic>,

View file

@ -571,6 +571,11 @@ impl Worktree {
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 {
@ -578,14 +583,6 @@ impl LocalWorktree {
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(
&mut self,
id: u64,
@ -1429,6 +1426,14 @@ impl Snapshot {
&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 {
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))
}
fn worktree_id(&self) -> usize {
self.worktree.id()
}
fn is_deleted(&self) -> bool {
self.is_deleted
}
@ -2424,6 +2433,17 @@ impl language::LocalFile for 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(
proto: rpc::proto::File,
worktree: ModelHandle<Worktree>,
@ -2485,7 +2505,7 @@ pub enum EntryKind {
File(CharBag),
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathChange {
/// A filesystem entry was was created.
Added,
@ -3617,7 +3637,7 @@ pub trait WorktreeHandle {
impl WorktreeHandle for ModelHandle<Worktree> {
// 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.
//
// 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
.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.
let fake_symbols = [
symbol("one", "/external"),

View file

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

View file

@ -132,6 +132,8 @@ message Envelope {
OnTypeFormatting on_type_formatting = 111;
OnTypeFormattingResponse on_type_formatting_response = 112;
UpdateWorktreeSettings update_worktree_settings = 113;
}
}
@ -339,6 +341,13 @@ message UpdateWorktree {
string abs_path = 10;
}
message UpdateWorktreeSettings {
uint64 project_id = 1;
uint64 worktree_id = 2;
string path = 3;
optional string content = 4;
}
message CreateProjectEntry {
uint64 project_id = 1;
uint64 worktree_id = 2;
@ -467,7 +476,7 @@ message Symbol {
string name = 4;
int32 kind = 5;
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
PointUtf16 start = 7;
PointUtf16 end = 8;

View file

@ -42,7 +42,7 @@ impl PublicKey {
}
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> {
let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE)
.context("failed to base64-decode encrypted string")?;

View file

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

View file

@ -25,7 +25,7 @@ use std::{
borrow::Cow,
collections::HashSet,
mem,
ops::Range,
ops::{Not, Range},
path::PathBuf,
sync::Arc,
};
@ -242,7 +242,13 @@ impl View for ProjectSearchView {
impl Item for ProjectSearchView {
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>(

View file

@ -12,7 +12,6 @@ doctest = false
test-support = ["gpui/test-support", "fs/test-support"]
[dependencies]
assets = { path = "../assets" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
@ -25,6 +24,7 @@ futures.workspace = true
json_comments = "0.2"
lazy_static.workspace = true
postage.workspace = true
rust-embed.workspace = true
schemars.workspace = true
serde.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 assets::Assets;
use collections::BTreeMap;
use gpui::{keymap_matcher::Binding, AppContext};
use schemars::{
@ -10,11 +9,11 @@ use schemars::{
};
use serde::Deserialize;
use serde_json::{value::RawValue, Value};
use util::ResultExt;
use util::{asset_str, ResultExt};
#[derive(Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
pub struct KeymapFileContent(Vec<KeymapBlock>);
pub struct KeymapFile(Vec<KeymapBlock>);
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock {
@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
#[derive(Deserialize)]
struct ActionWithData(Box<str>, Box<RawValue>);
impl KeymapFileContent {
impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
let content = Assets::get(asset_path).unwrap().data;
let content_str = std::str::from_utf8(content.as_ref()).unwrap();
Self::parse(content_str)?.add_to_cx(cx)
let content = asset_str::<SettingsAssets>(asset_path);
Self::parse(content.as_ref())?.add_to_cx(cx)
}
pub fn parse(content: &str) -> Result<Self> {
@ -83,40 +81,40 @@ impl KeymapFileContent {
}
Ok(())
}
}
pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value {
let mut root_schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<KeymapFileContent>();
pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
let mut root_schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<KeymapFile>();
let action_schema = Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(
action_names
.iter()
.map(|name| Value::String(name.to_string()))
.collect(),
),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
..Default::default()
}),
]),
let action_schema = Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(
action_names
.iter()
.map(|name| Value::String(name.to_string()))
.collect(),
),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
..Default::default()
}),
]),
..Default::default()
})),
..Default::default()
})),
..Default::default()
});
});
root_schema
.definitions
.insert("KeymapAction".to_owned(), action_schema);
root_schema
.definitions
.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_store;
use gpui::AssetSource;
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
use rust_embed::RustEmbed;
use std::{borrow::Cow, str};
use util::asset_str;
pub use keymap_file::KeymapFile;
pub use settings_file::*;
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
use std::{borrow::Cow, str};
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
#[derive(RustEmbed)]
#[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> {
match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
pub fn default_settings() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/default.json")
}
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 assets::Assets;
use fs::Fs;
use futures::{channel::mpsc, StreamExt};
use gpui::{executor::Background, AppContext, AssetSource};
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
use gpui::{executor::Background, AppContext};
use std::{
io::ErrorKind,
path::{Path, PathBuf},
str,
sync::Arc,
time::Duration,
};
use util::{paths, ResultExt};
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)
}
pub fn default_settings() -> Cow<'static, str> {
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
}
pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
cx.global::<SettingsStore>().get(location)
}
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"))]
pub fn test_settings() -> String {
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
default_settings().as_ref(),
crate::default_settings().as_ref(),
)
.unwrap();
util::merge_non_null_json_value_into(
@ -55,15 +57,22 @@ pub fn watch_config_file(
.spawn(async move {
let events = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await {
if !tx.unbounded_send(contents).is_ok() {
break;
}
}
if events.next().await.is_none() {
break;
}
}
})
.detach();
@ -101,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
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);

View file

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use collections::{btree_map, hash_map, BTreeMap, HashMap};
use gpui::AppContext;
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.
#[derive(Default)]
pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_deserialized_settings: Option<serde_json::Value>,
user_deserialized_settings: Option<serde_json::Value>,
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
default_deserialized_settings: serde_json::Value,
user_deserialized_settings: 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>>)>,
}
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)]
struct SettingValue<T> {
global_value: Option<T>,
local_values: Vec<(Arc<Path>, T)>,
local_values: Vec<(usize, Arc<Path>, T)>,
}
trait AnySettingValue {
@ -109,9 +120,9 @@ trait AnySettingValue {
custom: &[DeserializedSetting],
cx: &AppContext,
) -> 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_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(
&self,
generator: &mut SchemaGenerator,
@ -136,27 +147,24 @@ impl SettingsStore {
local_values: Vec::new(),
}));
if let Some(default_settings) = &self.default_deserialized_settings {
if let Some(default_settings) = setting_value
.deserialize_setting(default_settings)
if let Some(default_settings) = setting_value
.deserialize_setting(&self.default_deserialized_settings)
.log_err()
{
let mut user_values_stack = Vec::new();
if let Some(user_settings) = setting_value
.deserialize_setting(&self.user_deserialized_settings)
.log_err()
{
let mut user_values_stack = Vec::new();
user_values_stack = vec![user_settings];
}
if let Some(user_settings) = &self.user_deserialized_settings {
if let Some(user_settings) =
setting_value.deserialize_setting(user_settings).log_err()
{
user_values_stack = vec![user_settings];
}
}
if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx)
.log_err()
{
setting_value.set_global_value(setting);
}
if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx)
.log_err()
{
setting_value.set_global_value(setting);
}
}
}
@ -165,7 +173,7 @@ impl SettingsStore {
///
/// Panics if the given setting type has not been registered, or if there is no
/// 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
.get(&TypeId::of::<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,
/// use the typed setting interface.
pub fn untyped_user_settings(&self) -> &serde_json::Value {
self.user_deserialized_settings
.as_ref()
.unwrap_or(&serde_json::Value::Null)
&self.user_deserialized_settings
}
#[cfg(any(test, feature = "test-support"))]
@ -213,11 +219,7 @@ impl SettingsStore {
cx: &AppContext,
update: impl FnOnce(&mut T::FileContent),
) {
if self.user_deserialized_settings.is_none() {
self.set_user_settings("{}", cx).unwrap();
}
let old_text =
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
let new_text = self.new_text_for_update::<T>(old_text, update);
self.set_user_settings(&new_text, cx).unwrap();
}
@ -250,11 +252,7 @@ impl SettingsStore {
.setting_values
.get(&setting_type_id)
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.deserialize_setting(
self.user_deserialized_settings
.as_ref()
.expect("no user settings loaded"),
)
.deserialize_setting(&self.user_deserialized_settings)
.unwrap_or_else(|e| {
panic!(
"could not deserialize setting type {} from user settings: {}",
@ -323,10 +321,14 @@ impl SettingsStore {
default_settings_content: &str,
cx: &AppContext,
) -> Result<()> {
self.default_deserialized_settings =
Some(parse_json_with_comments(default_settings_content)?);
self.recompute_values(None, cx)?;
Ok(())
let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
if settings.is_object() {
self.default_deserialized_settings = settings;
self.recompute_values(None, cx)?;
Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
}
/// Set the user settings via a JSON string.
@ -335,28 +337,49 @@ impl SettingsStore {
user_settings_content: &str,
cx: &AppContext,
) -> Result<()> {
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
self.recompute_values(None, cx)?;
Ok(())
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)?;
Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
}
/// Add or remove a set of local settings via a JSON string.
pub fn set_local_settings(
&mut self,
root_id: usize,
path: Arc<Path>,
settings_content: Option<&str>,
cx: &AppContext,
) -> Result<()> {
if let Some(content) = settings_content {
self.local_deserialized_settings
.insert(path.clone(), parse_json_with_comments(content)?);
.insert((root_id, path.clone()), parse_json_with_comments(content)?);
} 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(())
}
/// 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(
&self,
schema_params: &SettingsJsonSchemaParams,
@ -436,72 +459,70 @@ impl SettingsStore {
fn recompute_values(
&mut self,
changed_local_path: Option<&Path>,
changed_local_path: Option<(usize, &Path)>,
cx: &AppContext,
) -> Result<()> {
// Reload the global and local values for every setting.
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() {
if let Some(default_settings) = &self.default_deserialized_settings {
let default_settings = setting_value.deserialize_setting(default_settings)?;
let default_settings =
setting_value.deserialize_setting(&self.default_deserialized_settings)?;
user_settings_stack.clear();
paths_stack.clear();
user_settings_stack.clear();
paths_stack.clear();
if let Some(user_settings) = &self.user_deserialized_settings {
if let Some(user_settings) =
setting_value.deserialize_setting(user_settings).log_err()
{
user_settings_stack.push(user_settings);
paths_stack.push(None);
if let Some(user_settings) = setting_value
.deserialize_setting(&self.user_deserialized_settings)
.log_err()
{
user_settings_stack.push(user_settings);
paths_stack.push(None);
}
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.log_err()
{
setting_value.set_global_value(value);
}
}
// Reload the local values for the setting.
for ((root_id, path), local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_entry) = paths_stack.last() {
if let Some((prev_root_id, prev_path)) = prev_entry {
if root_id != prev_root_id || !path.starts_with(prev_path) {
paths_stack.pop();
user_settings_stack.pop();
continue;
}
}
break;
}
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err()
{
paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
*root_id != changed_root_id || !path.starts_with(changed_local_path)
}) {
continue;
}
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.log_err()
{
setting_value.set_global_value(value);
}
}
// Reload the local values for the setting.
for (path, local_settings) in &self.local_deserialized_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_path) = paths_stack.last() {
if let Some(prev_path) = prev_path {
if !path.starts_with(prev_path) {
paths_stack.pop();
user_settings_stack.pop();
continue;
}
}
break;
}
if let Some(local_settings) =
setting_value.deserialize_setting(&local_settings).log_err()
{
paths_stack.push(Some(path.as_ref()));
user_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
if changed_local_path.map_or(false, |changed_local_path| {
!path.starts_with(changed_local_path)
}) {
continue;
}
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.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> {
fn key(&self) -> Option<&'static str> {
T::KEY
@ -546,10 +585,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
Ok(DeserializedSetting(Box::new(value)))
}
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
if let Some(path) = path {
for (settings_path, value) in self.local_values.iter().rev() {
if path.starts_with(&settings_path) {
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
if let Some((root_id, path)) = path {
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
if root_id == *settings_root_id && path.starts_with(&settings_path) {
return value;
}
}
@ -563,11 +602,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
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();
match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
Ok(ix) => self.local_values[ix].1 = value,
Err(ix) => self.local_values.insert(ix, (path, value)),
match self
.local_values
.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>(
text: &mut String,
key_path: &mut Vec<&'a str>,
@ -639,6 +665,10 @@ fn update_value_in_json_text<'a>(
key_path.pop();
}
} 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) =
replace_value_in_json_text(text, &key_path, tab_size, &new_value);
text.replace_range(range.clone(), &replacement);
@ -650,7 +680,7 @@ fn replace_value_in_json_text(
text: &str,
key_path: &[&str],
tab_size: usize,
new_value: impl Serialize,
new_value: &serde_json::Value,
) -> (Range<usize>, String) {
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
const LANGUAGES: &'static str = "languages";
@ -884,6 +914,7 @@ mod tests {
store
.set_local_settings(
1,
Path::new("/root1").into(),
Some(r#"{ "user": { "staff": true } }"#),
cx,
@ -891,6 +922,7 @@ mod tests {
.unwrap();
store
.set_local_settings(
1,
Path::new("/root1/subdir").into(),
Some(r#"{ "user": { "name": "Jane Doe" } }"#),
cx,
@ -899,6 +931,7 @@ mod tests {
store
.set_local_settings(
1,
Path::new("/root2").into(),
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
cx,
@ -906,7 +939,7 @@ mod tests {
.unwrap();
assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/something"))),
store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
&UserSettings {
name: "John Doe".to_string(),
age: 31,
@ -914,7 +947,7 @@ mod tests {
}
);
assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
&UserSettings {
name: "Jane Doe".to_string(),
age: 31,
@ -922,7 +955,7 @@ mod tests {
}
);
assert_eq!(
store.get::<UserSettings>(Some(Path::new("/root2/something"))),
store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
&UserSettings {
name: "John Doe".to_string(),
age: 42,
@ -930,7 +963,7 @@ mod tests {
}
);
assert_eq!(
store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
&MultiKeySettings {
key1: "a".to_string(),
key2: "b".to_string(),
@ -994,24 +1027,32 @@ mod tests {
r#"{
"languages": {
"JSON": {
"is_enabled": true
"language_setting_1": true
}
}
}"#
.unindent(),
|settings| {
settings.languages.get_mut("JSON").unwrap().is_enabled = false;
settings
.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#"{
"languages": {
"Rust": {
"is_enabled": true
"language_setting_2": true
},
"JSON": {
"is_enabled": false
"language_setting_1": false
}
}
}"#
@ -1074,6 +1115,23 @@ mod tests {
.unindent(),
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>(
@ -1202,9 +1260,10 @@ mod tests {
languages: HashMap<String, LanguageSettingEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
struct LanguageSettingEntry {
is_enabled: bool,
language_setting_1: Option<bool>,
language_setting_2: Option<bool>,
}
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
// so we can await on the result
let (sender, reciever) = oneshot::channel();
let (sender, receiver) = oneshot::channel();
let thread_safe_connection = (*self).clone();
write_channel(Box::new(move || {
@ -168,7 +168,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
let result = connection.with_write(|connection| callback(connection));
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(
@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor {
use std::sync::mpsc::channel;
Box::new(|| {
let (sender, reciever) = channel::<QueuedWrite>();
let (sender, receiver) = channel::<QueuedWrite>();
thread::spawn(move || {
while let Ok(write) = reciever.recv() {
while let Ok(write) = receiver.recv() {
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.
///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
pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
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
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,
257 => style.background,
258 => style.cursor,

View file

@ -37,8 +37,6 @@ lazy_static.workspace = true
serde.workspace = true
serde_derive.workspace = true
[dev-dependencies]
editor = { path = "../editor", 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()`.
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

View file

@ -40,7 +40,7 @@ function contrast_colour {
# Uncomment the below for more precise luminance calculations
# # Calculate percieved brightness
# # Calculate perceived brightness
# # See https://www.w3.org/TR/AERT#color-contrast
# # and http://www.itu.int/rec/R-REC-BT.601
# # 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;
///The information generated during layout that is nescessary for painting
///The information generated during layout that is necessary for painting
pub struct LayoutState {
cells: Vec<LayoutCell>,
rects: Vec<LayoutRect>,
@ -206,7 +206,7 @@ impl TerminalElement {
//Expand background rect range
{
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;
if let Some(rect) = cur_rect {
rects.push(rect);

View file

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

View file

@ -133,8 +133,8 @@ impl TerminalView {
Event::Wakeup => {
if !cx.is_self_focused() {
this.has_new_content = true;
cx.notify();
}
cx.notify();
cx.emit(Event::Wakeup);
}
Event::Bell => {
@ -804,7 +804,7 @@ mod tests {
let workspace = workspace.read(cx);
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!(workspace.worktrees(cx).next().is_none());
@ -825,7 +825,7 @@ mod tests {
let workspace = workspace.read(cx);
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!(workspace.worktrees(cx).next().is_some());
@ -905,7 +905,10 @@ mod tests {
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
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 (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));

View file

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

View file

@ -1749,6 +1749,12 @@ impl BufferSnapshot {
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<'_> {
let start = range.start.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 tooltip: TooltipStyle,
pub terminal: TerminalStyle,
pub assistant: AssistantStyle,
pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle,
pub color_scheme: ColorScheme,
@ -968,6 +969,23 @@ pub struct TerminalStyle {
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)]
pub struct FeedbackStyle {
pub submit_button: Interactive<ContainedText>,

View file

@ -21,6 +21,7 @@ isahc.workspace = true
smol.workspace = true
url = "2.2"
rand.workspace = true
rust-embed.workspace = true
tempdir = { workspace = true, optional = true }
serde.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 LOG: PathBuf = LOGS_DIR.join("Zed.log");
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 {

View file

@ -7,8 +7,10 @@ pub mod paths;
pub mod test;
use std::{
borrow::Cow,
cmp::{self, Ordering},
ops::{AddAssign, Range, RangeInclusive},
panic::Location,
pin::Pin,
task::{Context, Poll},
};
@ -129,11 +131,13 @@ where
{
type Ok = T;
#[track_caller]
fn log_err(self) -> Option<T> {
match self {
Ok(value) => Some(value),
Err(error) => {
log::error!("{:?}", error);
let caller = Location::caller();
log::error!("{}:{}: {:?}", caller.file(), caller.line(), error);
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
// https://github.com/rust-lang/rust/issues/87800
// 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 }
serde_json.workspace = true
assets = { path = "../assets" }
collections = { path = "../collections" }
command_palette = { path = "../command_palette" }
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 top_anchor = editor.scroll_manager.anchor().top_anchor;
let top_anchor = editor.scroll_manager.anchor().anchor;
editor.change_selections(None, cx, |s| {
s.replace_cursors_with(|snapshot| {
@ -756,7 +756,7 @@ mod test {
ˇ
The quick"})
.await;
// Indoc disallows trailing whitspace.
// Indoc disallows trailing whitespace.
cx.assert(" ˇ \nThe quick").await;
}

View file

@ -29,7 +29,7 @@ use tokio::{
use crate::state::Mode;
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.
#[cfg(feature = "neovim")]
lazy_static! {

View file

@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
cx.update_global(|store: &mut SettingsStore, cx| {
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

View file

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

View file

@ -175,6 +175,10 @@ impl Dock {
}
}
pub fn position(&self) -> DockPosition {
self.position
}
pub fn is_open(&self) -> bool {
self.is_open
}
@ -184,6 +188,12 @@ impl Dock {
.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> {
self.panel_entries
.iter()
@ -472,11 +482,22 @@ impl View for PanelButtons {
Flex::row()
.with_children(panels.into_iter().enumerate().map(
|(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()
.with_child(
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);
Flex::row()
.with_child(

View file

@ -1,5 +1,5 @@
use crate::{Toast, Workspace};
use collections::HashSet;
use collections::HashMap;
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
use std::{any::TypeId, ops::DerefMut};
@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
}
}
struct NotificationTracker {
notifications_sent: HashSet<TypeId>,
pub(crate) struct NotificationTracker {
notifications_sent: HashMap<TypeId, Vec<usize>>,
}
impl std::ops::Deref for NotificationTracker {
type Target = HashSet<TypeId>;
type Target = HashMap<TypeId, Vec<usize>>;
fn deref(&self) -> &Self::Target {
&self.notifications_sent
@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
impl NotificationTracker {
fn new() -> Self {
Self {
notifications_sent: HashSet::default(),
notifications_sent: Default::default(),
}
}
}
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>(
&mut self,
id: usize,
cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) {
if !cx
.global::<NotificationTracker>()
.contains(&TypeId::of::<V>())
{
if !self.has_shown_notification_once::<V>(id, cx) {
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)
@ -154,9 +163,10 @@ pub mod simple_message_notification {
use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
fonts::TextStyle,
impl_actions,
platform::{CursorStyle, MouseButton},
AppContext, Element, Entity, View, ViewContext,
AnyElement, AppContext, Element, Entity, View, ViewContext,
};
use menu::Cancel;
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 {
message: Cow<'static, str>,
message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>,
}
@ -204,7 +219,17 @@ pub mod simple_message_notification {
S: Into<Cow<'static, str>>,
{
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,
click_message: None,
}
@ -243,84 +268,90 @@ pub mod simple_message_notification {
enum MessageNotificationTag {}
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 has_click_action = on_click.is_some();
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
.with_child(
Text::new(message, theme.message.text.clone())
.contained()
.with_style(theme.message.container)
.aligned()
.top()
.left()
.flex(1., true),
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_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_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.constrained()
.with_height(
cx.font_cache().line_height(theme.message.text.font_size),
)
Flex::column()
.with_child(
Flex::row()
.with_child(
message
.contained()
.with_style(theme.message.container)
.aligned()
.top()
.flex_float(),
),
)
.with_children({
let style = theme.action_message.style_for(state, false);
if let Some(click_message) = click_message {
Some(
Flex::row().with_child(
Text::new(click_message, style.text.clone())
.left()
.flex(1., true),
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_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_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.constrained()
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
.aligned()
.top()
.flex_float(),
),
)
.with_children({
click_message
.map(|click_message| {
MouseEventHandler::<MessageNotificationTag, _>::new(
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state, false);
Flex::row()
.with_child(
Text::new(click_message, style.text.clone())
.contained()
.with_style(style.container),
)
.contained()
.with_style(style.container),
),
},
)
} else {
None
}
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(on_click) = on_click.as_ref() {
on_click(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 {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
})
.into_iter()
})
.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| {
if let Some(on_click) = on_click.as_ref() {
on_click(cx);
this.dismiss(&Default::default(), cx);
}
})
.with_cursor_style(if has_click_action {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
.into_any()
})
.into_any()
}
}

View file

@ -2,8 +2,8 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
use crate::{
item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile,
NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@ -268,6 +268,7 @@ impl Pane {
.with_child(Self::render_tab_bar_button(
0,
"icons/plus_12.svg",
false,
Some(("New...".into(), None)),
cx,
|pane, cx| pane.deploy_new_menu(cx),
@ -277,6 +278,7 @@ impl Pane {
.with_child(Self::render_tab_bar_button(
1,
"icons/split_12.svg",
false,
Some(("Split Pane".into(), None)),
cx,
|pane, cx| pane.deploy_split_menu(cx),
@ -290,6 +292,7 @@ impl Pane {
} else {
"icons/maximize_8.svg"
},
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
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>) {
// 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 {
cx.emit(Event::ZoomOut);
} else if !self.items.is_empty() {
@ -1014,7 +1022,7 @@ impl Pane {
let is_active_item = target_item_id == active_item_id;
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| {
menu.show(
@ -1143,7 +1151,8 @@ impl Pane {
let theme = theme::current(cx).clone();
let mut tooltip_theme = theme.tooltip.clone();
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| {
let tab_style =
@ -1401,6 +1410,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize,
icon: &'static str,
active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>,
on_click: F,
@ -1410,7 +1420,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
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)
.with_color(style.color)
.constrained()

View file

@ -15,7 +15,6 @@ mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context, Result};
use assets::Assets;
use call::ActiveCall;
use client::{
proto::{self, PeerId},
@ -60,7 +59,7 @@ use std::{
};
use crate::{
notifications::simple_message_notification::MessageNotification,
notifications::{simple_message_notification::MessageNotification, NotificationTracker},
persistence::model::{
DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
},
@ -81,9 +80,9 @@ use serde::Deserialize;
use shared_screen::SharedScreen;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use theme::Theme;
use theme::{Theme, ThemeSettings};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::{async_iife, paths, ResultExt};
use util::{async_iife, ResultExt};
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
lazy_static! {
@ -133,8 +132,6 @@ actions!(
]
);
actions!(zed, [OpenSettings]);
#[derive(Clone, PartialEq)]
pub struct OpenPaths {
pub paths: Vec<PathBuf>,
@ -295,17 +292,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
.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;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@ -765,25 +751,21 @@ impl Workspace {
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
.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(|| {
let window_bounds_override = window_bounds_env_override(&cx);
let (bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None)
} else {
@ -819,7 +801,14 @@ impl Workspace {
// Use the serialized workspace to construct the new window
cx.add_window(
(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
});
@ -908,18 +897,24 @@ impl Workspace {
}
});
} 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));
if panel.has_focus(cx) {
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx));
}
} 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) {
let position = panel.read(cx).position(cx);
this.dismiss_zoomed_items_to_reveal(Some(position), cx);
if panel.is_zoomed(cx) {
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx));
this.zoomed_position = Some(position);
} else {
this.zoomed = None;
this.zoomed_position = None;
@ -968,9 +963,8 @@ impl Workspace {
let timestamp = entry.timestamp;
match history.entry(project_path) {
hash_map::Entry::Occupied(mut entry) => {
let (old_fs_path, old_timestamp) = entry.get();
let (_, old_timestamp) = entry.get();
if &timestamp > old_timestamp {
assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
entry.insert((fs_path, timestamp));
}
}
@ -1592,7 +1586,7 @@ impl Workspace {
DockPosition::Right => &self.right_dock,
};
let mut focus_center = false;
let mut zoom_out = false;
let mut reveal_dock = false;
dock.update(cx, |dock, cx| {
let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
let was_visible = dock.is_open() && !other_is_zoomed;
@ -1607,14 +1601,15 @@ impl Workspace {
if active_panel.is_zoomed(cx) {
cx.focus(active_panel.as_any());
}
zoom_out = true;
reveal_dock = true;
}
}
});
if zoom_out {
self.zoom_out_everything_except(dock_side, cx);
if reveal_dock {
self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
}
if focus_center {
cx.focus_self();
}
@ -1623,62 +1618,49 @@ impl Workspace {
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>> {
self.show_or_hide_panel::<T>(cx, |_, _| true)?
self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
.as_any()
.clone()
.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>) {
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,
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>> {
for (dock, position) in [
self.left_dock.clone(),
self.bottom_dock.clone(),
self.right_dock.clone(),
]
.into_iter()
.zip(
[
DockPosition::Left,
DockPosition::Bottom,
DockPosition::Right,
]
.into_iter(),
) {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
let mut focus_center = false;
let mut zoom_out = false;
let mut reveal_dock = false;
let panel = dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx);
let panel = dock.active_panel().cloned();
if let Some(panel) = panel.as_ref() {
let should_show = show(&**panel, cx);
if should_show {
if should_focus(&**panel, cx) {
dock.set_open(true, cx);
cx.focus(panel.as_any());
zoom_out = true;
reveal_dock = true;
} else {
if panel.is_zoomed(cx) {
dock.set_open(false, cx);
}
// if panel.is_zoomed(cx) {
// dock.set_open(false, cx);
// }
focus_center = true;
}
}
panel
});
if zoom_out {
self.zoom_out_everything_except(position, cx);
}
if focus_center {
cx.focus_self();
}
@ -1691,6 +1673,16 @@ impl Workspace {
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>) {
for pane in &self.panes {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
@ -1705,28 +1697,38 @@ impl Workspace {
cx.notify();
}
fn zoom_out_everything_except(
fn dismiss_zoomed_items_to_reveal(
&mut self,
except_position: DockPosition,
dock_to_reveal: Option<DockPosition>,
cx: &mut ViewContext<Self>,
) {
// If a center pane is zoomed, unzoom it.
for pane in &self.panes {
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
if pane != &self.active_pane || dock_to_reveal.is_some() {
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 another dock is zoomed, hide it.
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::Bottom {
self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
if focus_center {
cx.focus_self();
}
if except_position != DockPosition::Right {
self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
}
if self.zoomed_position != Some(except_position) {
if self.zoomed_position != dock_to_reveal {
self.zoomed = None;
self.zoomed_position = None;
}
@ -1937,6 +1939,7 @@ impl Workspace {
self.last_active_center_pane = Some(pane.downgrade());
}
self.dismiss_zoomed_items_to_reveal(None, cx);
if pane.read(cx).is_zoomed() {
self.zoomed = Some(pane.downgrade().into_any());
} else {
@ -1998,7 +2001,6 @@ impl Workspace {
}
pane::Event::ZoomIn => {
if pane == self.active_pane {
self.zoom_out(cx);
pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
if pane.read(cx).has_focus() {
self.zoomed = Some(pane.downgrade().into_any());
@ -2007,7 +2009,13 @@ impl Workspace {
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);
@ -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(
serialized_workspace: Option<SerializedWorkspace>,
workspace: &WeakViewHandle<Workspace>,
@ -3190,6 +3209,87 @@ async fn open_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) {
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 {
let backup_path = (*db::BACKUP_DB_PATH).read();
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 |_| {
MessageNotification::new(format!(
"Database file was corrupted. Old database backed up to {}",
@ -3278,32 +3378,36 @@ impl View for Workspace {
enum ZoomBackground {}
let zoomed = zoomed.upgrade(cx)?;
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;
let mut foreground_style =
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?;
let window_bounds_override = window_bounds_env_override(&cx);
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),
);
(app_state.initialize_workspace)(
@ -4257,6 +4366,12 @@ mod tests {
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
workspace.update(cx, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
@ -4324,7 +4439,7 @@ mod tests {
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.toggle_panel_focus::<TestPanel>(cx);
});
@ -4358,6 +4473,25 @@ mod tests {
assert!(workspace.zoomed.is_some());
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]

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