Compare commits

...

42 Commits

Author SHA1 Message Date
Max Brunsfeld
d063874f49 Merge branch 'main' into always-adjust-selections 2024-10-28 11:38:03 -07:00
Thorsten Ball
fab2f22a89 remote project: Fix project reference leak when waiting for prompt reply (#19838)
When the language server gave us a prompt and we'd close the window, we
wouldn't release the `project` until the next `flush_effects` call that
came in when opening a window.

With this change, we no longer hold a strong reference to the project in
the future. Fixes the leak and makes sure we clean up the SSH connection
when closing a window.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 17:07:30 +01:00
Marshall Bowers
a451bcc3c4 collab: Exempt staff from LLM usage limits (#19836)
This PR updates the usage limit check to exempt Zed staff members from
usage limits.

We previously had some affordances for the rate limits, but hadn't yet
updated it for the usage-based billing.

Release Notes:

- N/A
2024-10-28 11:45:18 -04:00
Marshall Bowers
5e9ff3e313 dart: Bump to v0.1.2 (#19835)
This PR bumps the Dart extension to v0.1.2.

Changes:

- https://github.com/zed-industries/zed/pull/19592

Release Notes:

- N/A
2024-10-28 11:36:44 -04:00
Thorsten Ball
cc81f19c68 remote server: Fix error log about inability to open buffer (#19824)
Turns out that we used client-side `fs` to check whether something is a
directory or not, which obviously doesn't work with SSH projects.

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 16:35:37 +01:00
Ömer Sinan Ağacan
5e89fba681 dart: Add support for documentation comments (#19592)
Closes #19590

Release Notes:

- N/A
---

I'm unable to test this because rebuilding Zed with the changes does not
seem to use the changes. If maintainers could let me know how to test
these changes I'd like to verify that this really fixes #19590.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-28 11:20:04 -04:00
Thorsten Ball
67eb652bf1 remote servers: Always dismiss modal (#19831)
We display the errors in another window anyway and if the connection
takes a while it looks like a bug that the modal stays open.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 16:12:37 +01:00
Bennet Bo Fenner
e0ea9a9ab5 Remove leftover comments from previous PR (#19820)
Co-Authored-by: Thorsten <thorsten@zed.dev>

Removes some leftover comments from #19766 

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-28 16:00:38 +01:00
xdBronch
ff29a34298 zig: Account for doctests in outline (#19776)
zig has a feature called
[doctests](https://ziglang.org/documentation/master/#Doctests) where
instead of providing a string as the name of a test you use an
identifier so that the test is "tied" to it and can be used in
documentation. this wasnt accounted for so any tests using this were
unnamed in the outline

Release Notes:

- N/A
2024-10-28 10:49:40 -04:00
Thorsten Ball
6686f66949 ollama: Ensure only single task fetches models (#19830)
Before this change, we'd see a ton of requests from the Ollama provider
trying to fetch models:

```
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: https://api.zed.dev/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
```

Turns out we'd send a request on *every* change to settings.

Now, with this change, we only send a single request.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 15:40:50 +01:00
David Soria Parra
8a96ea25c4 context_servers: Support tools (#19548)
This PR depends on #19547 

This PR adds support for tools from context servers. Context servers are
free to expose tools that Zed can pass to models. When called by the
model, Zed forwards the request to context servers. This allows for some
interesting techniques. Context servers can easily expose tools such as
querying local databases, reading or writing local files, reading
resources over authenticated APIs (e.g. kubernetes, asana, etc).

This is currently experimental. 

Things to discuss
* I want to still add a confirm dialog asking people if a server is
allows to use the tool. Should do this or just use the tool and assume
trustworthyness of context servers?
* Can we add tool use behind a local setting flag?

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-28 10:37:58 -04:00
Piotr Osiewicz
cdddb4d360 Add language toolchains (#19576)
This PR adds support for selecting toolchains for a given language (e.g.
Rust toolchains or Python virtual environments) with support for SSH
projects provided out of the box. For Python we piggy-back off of
[PET](https://github.com/microsoft/python-environment-tools), a library
maintained by Microsoft.
Closes #16421
Closes #7646

Release Notes:

- Added toolchain selector to the status bar (with initial support for
Python virtual environments)
2024-10-28 15:34:03 +01:00
Thorsten Ball
03bd95405b docs: Add diagram to remote development docs (#19827)
Release Notes:

- N/A
2024-10-28 14:14:51 +01:00
Kirill Bulatov
177dfdf900 Declare RUSTFLAGS env var for all CI jobs (#19826)
Follow-up of https://github.com/zed-industries/zed/pull/19149 

Makes RUSTFLAGS propagation uniform, to ensure all `cargo ...` jobs get
the same RUSTFLAGS env set.

Release Notes:

- N/A
2024-10-28 14:53:40 +02:00
Thorsten Ball
2ab0b3b819 remote server: Fix language servers not starting (#19821)
PR #19653 change the code in this diff, which lead to the remote_server
binary trying to load language grammars, which in turn failed, and
stopped languages from being loaded correctly.

That then lead to language servers not starting up.

This change reintroduces what #19653 removed, so that we don't load the
grammar on the remote_server, by ignoring the grammar name from the
config. The tests still all work.


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 11:02:46 +01:00
Bennet Bo Fenner
888fec9299 outline panel: Add indent guides (#19719)
See #12673

| File | Search |
|--------|--------|
| <img width="302" alt="image"
src="https://github.com/user-attachments/assets/44b8d5f9-8446-41b5-8c0f-e438050f0ac9">
| <img width="301" alt="image"
src="https://github.com/user-attachments/assets/a2e6f77b-6d3b-4f1c-8fcb-16bd35274807">
|



Release Notes:

- Added indent guides to the outline panel
2024-10-28 09:54:18 +01:00
Bennet Bo Fenner
e86b096b92 docs: Add indent_guides setting to project panel docs (#19819)
Follow up to #18260 

Release Notes:

- N/A
2024-10-28 09:45:19 +01:00
Mikayla Maki
ffe36c9beb Remove hosted projects (#19754)
Release Notes:

- N/A
2024-10-27 19:44:21 -07:00
Kirill Bulatov
2d16d2d036 Fixed outline panel panicking on filtering (#19811)
Closes https://github.com/zed-industries/zed/issues/19732

Release Notes:

- Fixed outline panel panicking on filtering
([#19732](https://github.com/zed-industries/zed/issues/19732))
2024-10-28 02:31:02 +02:00
Mikayla Maki
c69da2df70 Add support for git branches on remote projects (#19755)
Release Notes:

- Fixed a bug where the branch switcher could not be used remotely.
2024-10-27 15:50:54 -07:00
CharlesChen0823
5506669b06 windows: Fix more windows platform test (#19802)
Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-10-27 19:15:23 +02:00
Bennet Bo Fenner
b13940720a markdown preview: Ignore inline HTML tags in text (#19804)
Follow up to #19785

This PR ensures that we explicitly ignore inline HTML tags so that we
can still extract the text between the tags and show them to the user

Release Notes:

- N/A
2024-10-27 14:34:59 +01:00
Peter Tripp
db61711753 ci: Don't run GitHub Actions workflows on forks (#19789)
- Closes: https://github.com/zed-industries/zed/issues/19351

Release Notes:

- N/A
2024-10-27 09:04:52 -04:00
Joseph T. Lyons
c12a9f2673 Add fold_at_level test (#19800) 2024-10-26 21:57:55 -04:00
Kirill Bulatov
2e32f1c8a1 Restore horizontal scrollbar checks (#19767)
Closes https://github.com/zed-industries/zed/issues/19637

Follow-up of https://github.com/zed-industries/zed/pull/18927 , restores
the condition that removed the horizontal scrollbar when panel's items
are not long enough.

Release Notes:

- Fixed horizontal scrollbar not being hidden
([#19637](https://github.com/zed-industries/zed/issues/19637))
2024-10-26 21:57:22 +03:00
Bennet Bo Fenner
03a1c8d2b8 markdown preview: Fix infinite loop in parser when parsing list items (#19785)
Release Notes:

- Fixed an issue with the markdown parser when opening a markdown
preview file that contained HTML tags inside a list item
2024-10-26 14:59:46 +02:00
Kirill Bulatov
d7a277607b Fix a few Windows tests (#19773) 2024-10-26 03:32:22 +03:00
Thorsten Ball
fc8a72cdd8 WIP: ssh remoting: Add upload_binary field to SshConnections (#19748)
This removes the old `remote_server { "download_binary_on_host": bool }`
field and replaces it with a `upload_binary: bool` on every
`ssh_connection`.


@ConradIrwin it compiles, it connects, but I haven't tested it really
yet

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-10-25 17:32:54 -06:00
Kirill Bulatov
1acebb3c47 Remove another false-positive Danger message (#19769)
Follow-up of https://github.com/zed-industries/zed/pull/19151

Ignores any URLs aftrer `Release Notes:` (if present) and after
`Follow-up of` and `Part of` words.


Release Notes:

- N/A
2024-10-26 01:55:46 +03:00
Conrad Irwin
78ed0c9312 vim: Copy comment to new lines with o/O (#19766)
Co-Authored-By: Kurt Wolf <kurtwolfbuilds@gmail.com>

Closes: #4691

Closes #ISSUE

Release Notes:

- vim: o/O now respect `extend_comment_on_newline`
2024-10-25 16:47:44 -06:00
Conrad Irwin
98d2e5fe73 Quote fixes (#19765)
Closes #19372

Release Notes:

- Fixed autoclosing quotes when the string is already open.
- Added autoclosing of rust multiline strings

---------

Co-authored-by: Kurt Wolf <kurtwolfbuilds@gmail.com>
2024-10-25 16:28:08 -06:00
Max Brunsfeld
4325819075 Fix more failure cases of assistant edits (#19653)
* Make `description` optional (since we describe it as optional in the
prompt, and we're currently not showing it)
* Fix fuzzy location bug that neglected the cost of deleting prefixes of
the query.
* Make auto-indent work for single-line edits. Previously, auto-indent
would not occur when overwriting a single line (without inserting or
deleting a newline)

Release Notes:

- N/A
2024-10-25 14:30:34 -07:00
Marshall Bowers
c19c89e6df collab: Include checkout_complete query parameter after checking out (#19763)
This PR updates the checkout flow to include the `?checkout_complete=1`
query parameter after successfully checking out.

We'll use this on the account page to adapt the UI accordingly.

Release Notes:

- N/A
2024-10-25 16:29:20 -04:00
Joseph T. Lyons
507929cb79 Add editor: fold at level <level> commands (#19750)
Closes https://github.com/zed-industries/zed/issues/5142

Note that I only moved the cursor to the top of the file so it wouldn't
jump - the commands work no matter where you are in the file.


https://github.com/user-attachments/assets/78c74ca6-5c17-477c-b5d1-97c5665e44b0

Also, is VS Code doing this right thing here? or is it busted?


https://github.com/user-attachments/assets/8c503b50-9671-4221-b9f8-1e692fe8cd9a

Release Notes:

- Added `editor: fold at level <level>` commands. macOS: `cmd-k,
cmd-<number>`, Linux: `ctrl-k, ctrl-<number>`.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-10-25 16:08:37 -04:00
Max Brunsfeld
7d0a7aff44 Fix condition for re-using highlights when seeking buffer chunks iterator (#19760)
Fixes a syntax highlighting regression introduced in
https://github.com/zed-industries/zed/pull/19531, which caused syntax
highlighting to be missing after any block.

Release Notes:

- N/A
2024-10-25 12:37:03 -07:00
Kirill Bulatov
92ba18342c Properly deserialize active pane in the workspace (#19744)
Without setting the active pane metadata, no center pane events are
emitted on start before the pane is focused manually, which breaks
deserialization of other components like outline panel, which should
show the active pane's active item outlines on start.

Release Notes:

- N/A

Co-authored-by: Thorsten Ball <thorsten@zed.dev>
2024-10-25 22:04:09 +03:00
Kirill Bulatov
6de5ace116 Update outline panel representation when a theme is changed (#19747)
Release Notes:

- N/A
2024-10-25 22:04:02 +03:00
Richard Feldman
c9db1b9a7b Add keybindings for accepting hunks (#19749)
I went with Cmd-Shift-Y on macOS (Ctrl-Shift-Y on Linux) for "yes accept
this individual hunk" - both are currently unused.

I went with Cmd-Shift-A on macOS (Ctrl-Alt-A on Linux) for "accept all
hunks" - both are unused. (Ctrl-Shift-A on Linux was taken, as is
Ctrl-Alt-Y, so although the pairing of Ctrl-Shift-Y and Ctrl-Alt-A isn't
necessarily obvious, the letters seem intuitive - "yes" and "all" - and
those key combinations don't conflict with anything.)

Release Notes:

- Added keybindings for applying hunks in Proposed Changes
<img width="247" alt="Screenshot 2024-10-25 at 12 47 00 PM"
src="https://github.com/user-attachments/assets/d6355621-ba80-4ee2-8918-b7239a4d29be">
2024-10-25 14:10:04 -04:00
Nathan Sobo
24cb694494 Update placeholder text with key bindings to focus context panel and navigate history (#19447)
Hopefully, this will help people understand how easy it is to add
context to an inline transformation.

![CleanShot 2024-10-18 at 22 41
00@2x](https://github.com/user-attachments/assets/c09c1d89-3df2-4079-9849-9de7ac63c003)

@as-cii @maxdeviant @rtfeldman could somebody update this to display the
actual correct key bindings and ship it. I have them hard coded for now.

Release Notes:

- Updated placeholder text with key bindings to focus context panel and
navigate history.

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-10-25 14:09:21 -04:00
Conrad Irwin
85bdd9329b Revert "Show invisibles in editor (#19298)" (#19752)
Closes: #19714

This reverts commit 6dcec47235.

Release Notes:

- (preview only) Fixes a crash when rendering invisibles
2024-10-25 11:59:22 -06:00
Antonio Scandurra
5d7b63ca87 WIP 2024-10-25 19:09:13 +02:00
Antonio Scandurra
29db144434 Remove SelectionsCollection::all_adjusted in favor of using all
We still need to re-implement the adjustment logic in `all`, but this commit
paves the way for that.

Co-Authored-By: Nathan <nathan@zed.dev>
2024-10-25 16:45:30 +02:00
145 changed files with 5654 additions and 2263 deletions

View File

@@ -25,6 +25,7 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
RUSTFLAGS: "-D warnings"
jobs:
migration_checks:
@@ -91,6 +92,7 @@ jobs:
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- test
@@ -115,17 +117,18 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build collab
run: RUSTFLAGS="-D warnings" cargo build -p collab
run: cargo build -p collab
- name: Build other binaries and features
run: |
RUSTFLAGS="-D warnings" cargo build --workspace --bins --all-features
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
RUSTFLAGS="-D warnings" cargo build -p remote_server
cargo build -p remote_server
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
@@ -153,11 +156,12 @@ jobs:
uses: ./.github/actions/run_tests
- name: Build Zed
run: RUSTFLAGS="-D warnings" cargo build -p zed
run: cargo build -p zed
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
@@ -179,14 +183,18 @@ jobs:
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
run: RUSTFLAGS="-D warnings" cargo build -p remote_server
run: cargo build -p remote_server
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
@@ -203,7 +211,7 @@ jobs:
run: cargo xtask clippy
- name: Build Zed
run: $env:RUSTFLAGS="-D warnings"; cargo build
run: cargo build
bundle-mac:
timeout-minutes: 60

499
Cargo.lock generated
View File

@@ -291,6 +291,12 @@ dependencies = [
"syn 2.0.76",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.8"
@@ -385,7 +391,7 @@ dependencies = [
"ctor",
"db",
"editor",
"env_logger",
"env_logger 0.11.5",
"feature_flags",
"fs",
"futures 0.3.30",
@@ -2551,7 +2557,7 @@ dependencies = [
"dashmap 6.0.1",
"derive_more",
"editor",
"env_logger",
"env_logger 0.11.5",
"envy",
"file_finder",
"fs",
@@ -2706,7 +2712,7 @@ dependencies = [
"command_palette_hooks",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"fuzzy",
"go_to_line",
"gpui",
@@ -3483,7 +3489,7 @@ dependencies = [
"collections",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
@@ -3671,7 +3677,7 @@ dependencies = [
"ctor",
"db",
"emojis",
"env_logger",
"env_logger 0.11.5",
"file_icons",
"futures 0.3.30",
"fuzzy",
@@ -3711,7 +3717,6 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
"unicode-segmentation",
"unindent",
"url",
"util",
@@ -3878,6 +3883,19 @@ dependencies = [
"regex",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "env_logger"
version = "0.11.5"
@@ -3986,7 +4004,7 @@ dependencies = [
"client",
"clock",
"collections",
"env_logger",
"env_logger 0.11.5",
"feature_flags",
"fs",
"git",
@@ -4081,7 +4099,7 @@ dependencies = [
"client",
"collections",
"ctor",
"env_logger",
"env_logger 0.11.5",
"fs",
"futures 0.3.30",
"gpui",
@@ -4123,7 +4141,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"env_logger",
"env_logger 0.11.5",
"extension",
"fs",
"language",
@@ -4282,7 +4300,7 @@ dependencies = [
"collections",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"file_icons",
"futures 0.3.30",
"fuzzy",
@@ -5037,7 +5055,7 @@ dependencies = [
"ctor",
"derive_more",
"embed-resource",
"env_logger",
"env_logger 0.11.5",
"etagere",
"filedescriptor",
"flume",
@@ -5227,6 +5245,15 @@ dependencies = [
"serde",
]
[[package]]
name = "hashlink"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "hashlink"
version = "0.9.1"
@@ -6185,7 +6212,7 @@ dependencies = [
"collections",
"ctor",
"ec4rs",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"fuzzy",
"git",
@@ -6242,7 +6269,7 @@ dependencies = [
"copilot",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"feature_flags",
"futures 0.3.30",
"google_ai",
@@ -6299,7 +6326,7 @@ dependencies = [
"collections",
"copilot",
"editor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
@@ -6333,6 +6360,11 @@ dependencies = [
"lsp",
"node_runtime",
"paths",
"pet",
"pet-conda",
"pet-core",
"pet-poetry",
"pet-reporter",
"project",
"regex",
"rope",
@@ -6629,7 +6661,7 @@ dependencies = [
"async-pipe",
"collections",
"ctor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"log",
@@ -6712,7 +6744,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"language",
@@ -6825,7 +6857,7 @@ dependencies = [
"clap",
"clap_complete",
"elasticlunr-rs",
"env_logger",
"env_logger 0.11.5",
"futures-util",
"handlebars 5.1.2",
"ignore",
@@ -7007,6 +7039,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "msvc_spectre_libs"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8661ace213a0a130c7c5b9542df5023aedf092a02008ccf477b39ff108990305"
dependencies = [
"cc",
]
[[package]]
name = "multi_buffer"
version = "0.1.0"
@@ -7015,7 +7056,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"itertools 0.13.0",
@@ -7729,8 +7770,10 @@ dependencies = [
"serde",
"serde_json",
"settings",
"smallvec",
"smol",
"theme",
"ui",
"util",
"workspace",
"worktree",
@@ -7973,6 +8016,366 @@ dependencies = [
"sha2",
]
[[package]]
name = "pet"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"clap",
"env_logger 0.10.2",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-env-var-path",
"pet-fs",
"pet-global-virtualenvs",
"pet-homebrew",
"pet-jsonrpc",
"pet-linux-global-python",
"pet-mac-commandlinetools",
"pet-mac-python-org",
"pet-mac-xcode",
"pet-pipenv",
"pet-poetry",
"pet-pyenv",
"pet-python-utils",
"pet-reporter",
"pet-telemetry",
"pet-venv",
"pet-virtualenv",
"pet-virtualenvwrapper",
"pet-windows-registry",
"pet-windows-store",
"serde",
"serde_json",
]
[[package]]
name = "pet-conda"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"env_logger 0.10.2",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-reporter",
"regex",
"serde",
"serde_json",
"yaml-rust2",
]
[[package]]
name = "pet-core"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"clap",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-fs",
"regex",
"serde",
"serde_json",
]
[[package]]
name = "pet-env-var-path"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
"regex",
]
[[package]]
name = "pet-fs"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
]
[[package]]
name = "pet-global-virtualenvs"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-fs",
"pet-virtualenv",
]
[[package]]
name = "pet-homebrew"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
"regex",
"serde",
"serde_json",
]
[[package]]
name = "pet-jsonrpc"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"env_logger 0.10.2",
"log",
"msvc_spectre_libs",
"pet-core",
"serde",
"serde_json",
]
[[package]]
name = "pet-linux-global-python"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-mac-commandlinetools"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-mac-python-org"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-mac-xcode"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-pipenv"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-poetry"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"base64 0.22.1",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-reporter",
"pet-virtualenv",
"regex",
"serde",
"serde_json",
"sha2",
"toml 0.8.19",
]
[[package]]
name = "pet-pyenv"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-reporter",
"regex",
"serde",
"serde_json",
]
[[package]]
name = "pet-python-utils"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"env_logger 0.10.2",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"regex",
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "pet-reporter"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"env_logger 0.10.2",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-jsonrpc",
"serde",
"serde_json",
]
[[package]]
name = "pet-telemetry"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"env_logger 0.10.2",
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"regex",
]
[[package]]
name = "pet-venv"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-virtualenv"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
]
[[package]]
name = "pet-virtualenvwrapper"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
]
[[package]]
name = "pet-windows-registry"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-conda",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
"pet-windows-store",
"regex",
"winreg 0.52.0",
]
[[package]]
name = "pet-windows-store"
version = "0.1.0"
source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c"
dependencies = [
"lazy_static",
"log",
"msvc_spectre_libs",
"pet-core",
"pet-fs",
"pet-python-utils",
"pet-virtualenv",
"regex",
"winreg 0.52.0",
]
[[package]]
name = "petgraph"
version = "0.6.5"
@@ -8061,7 +8464,7 @@ dependencies = [
"anyhow",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"gpui",
"menu",
"serde",
@@ -8407,7 +8810,7 @@ dependencies = [
"client",
"clock",
"collections",
"env_logger",
"env_logger 0.11.5",
"fs",
"futures 0.3.30",
"fuzzy",
@@ -9122,7 +9525,7 @@ dependencies = [
"clap",
"client",
"clock",
"env_logger",
"env_logger 0.11.5",
"fork",
"fs",
"futures 0.3.30",
@@ -9173,7 +9576,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"editor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"http_client",
@@ -9453,7 +9856,7 @@ dependencies = [
"arrayvec",
"criterion",
"ctor",
"env_logger",
"env_logger 0.11.5",
"gpui",
"log",
"rand 0.8.5",
@@ -9484,7 +9887,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"collections",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"parking_lot",
@@ -10073,7 +10476,7 @@ dependencies = [
"client",
"clock",
"collections",
"env_logger",
"env_logger 0.11.5",
"feature_flags",
"fs",
"futures 0.3.30",
@@ -10766,7 +11169,7 @@ dependencies = [
"futures-io",
"futures-util",
"hashbrown 0.14.5",
"hashlink",
"hashlink 0.9.1",
"hex",
"indexmap 2.4.0",
"log",
@@ -11090,7 +11493,7 @@ version = "0.1.0"
dependencies = [
"arrayvec",
"ctor",
"env_logger",
"env_logger 0.11.5",
"log",
"rand 0.8.5",
"rayon",
@@ -11104,7 +11507,7 @@ dependencies = [
"client",
"collections",
"editor",
"env_logger",
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"http_client",
@@ -11403,7 +11806,7 @@ dependencies = [
"collections",
"ctor",
"editor",
"env_logger",
"env_logger 0.11.5",
"gpui",
"language",
"menu",
@@ -11610,7 +12013,7 @@ dependencies = [
"clock",
"collections",
"ctor",
"env_logger",
"env_logger 0.11.5",
"gpui",
"http_client",
"log",
@@ -12099,6 +12502,21 @@ dependencies = [
"winnow 0.6.18",
]
[[package]]
name = "toolchain_selector"
version = "0.1.0"
dependencies = [
"editor",
"fuzzy",
"gpui",
"language",
"picker",
"project",
"ui",
"util",
"workspace",
]
[[package]]
name = "topological-sort"
version = "0.2.2"
@@ -12844,6 +13262,7 @@ dependencies = [
"git",
"gpui",
"picker",
"project",
"ui",
"util",
"workspace",
@@ -14267,7 +14686,7 @@ dependencies = [
"collections",
"db",
"derive_more",
"env_logger",
"env_logger 0.11.5",
"fs",
"futures 0.3.30",
"git",
@@ -14304,7 +14723,7 @@ dependencies = [
"anyhow",
"clock",
"collections",
"env_logger",
"env_logger 0.11.5",
"fs",
"futures 0.3.30",
"fuzzy",
@@ -14474,6 +14893,17 @@ dependencies = [
"clap",
]
[[package]]
name = "yaml-rust2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink 0.8.4",
]
[[package]]
name = "yansi"
version = "1.0.1"
@@ -14587,7 +15017,7 @@ dependencies = [
"db",
"diagnostics",
"editor",
"env_logger",
"env_logger 0.11.5",
"extension",
"extensions_ui",
"feature_flags",
@@ -14654,6 +15084,7 @@ dependencies = [
"theme",
"theme_selector",
"time",
"toolchain_selector",
"tree-sitter-md",
"tree-sitter-rust",
"ui",
@@ -14701,7 +15132,7 @@ dependencies = [
[[package]]
name = "zed_dart"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -117,6 +117,7 @@ members = [
"crates/theme_selector",
"crates/time_format",
"crates/title_bar",
"crates/toolchain_selector",
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
@@ -290,6 +291,7 @@ theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
@@ -376,6 +378,11 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
profiling = "1"
@@ -464,7 +471,7 @@ tree-sitter-typescript = "0.23"
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
unindent = "0.1.7"
unicode-segmentation = "1.11"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.215"

View File

@@ -313,6 +313,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", { "level": 1 }],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", { "level": 2 }],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", { "level": 3 }],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", { "level": 4 }],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", { "level": 5 }],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", { "level": 6 }],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", { "level": 7 }],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", { "level": 8 }],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", { "level": 9 }],
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -505,6 +514,13 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ProposedChangesEditor",
"bindings": {
"ctrl-shift-y": "editor::ApplyDiffHunk",
"ctrl-alt-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {

View File

@@ -349,7 +349,15 @@
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
"cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
"cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
"cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],
"cmd-k cmd-4": ["editor::FoldAtLevel", { "level": 4 }],
"cmd-k cmd-5": ["editor::FoldAtLevel", { "level": 5 }],
"cmd-k cmd-6": ["editor::FoldAtLevel", { "level": 6 }],
"cmd-k cmd-7": ["editor::FoldAtLevel", { "level": 7 }],
"cmd-k cmd-8": ["editor::FoldAtLevel", { "level": 8 }],
"cmd-k cmd-9": ["editor::FoldAtLevel", { "level": 9 }],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -538,6 +546,13 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ProposedChangesEditor",
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "PromptEditor",
"bindings": {

View File

@@ -88,7 +88,6 @@ origin: (f64, f64),
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Update the Rectangle's new function to take an origin parameter</description>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
@@ -117,7 +116,6 @@ pub struct Circle {
<edit>
<path>src/shapes/circle.rs</path>
<description>Update the Circle's new function to take an origin parameter</description>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
@@ -134,7 +132,6 @@ fn new(origin: (f64, f64), radius: f64) -> Self {
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add an import for the std::fmt module</description>
<operation>insert_before</operation>
<old_text>
struct Rectangle {
@@ -147,7 +144,10 @@ use std::fmt;
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add a Display implementation for Rectangle</description>
<description>
Add a manual Display implementation for Rectangle.
Currently, this is the same as a derived Display implementation.
</description>
<operation>insert_after</operation>
<old_text>
Rectangle { width, height }
@@ -169,7 +169,6 @@ impl fmt::Display for Rectangle {
<edit>
<path>src/shapes/circle.rs</path>
<description>Add an import for the `std::fmt` module</description>
<operation>insert_before</operation>
<old_text>
struct Circle {
@@ -181,7 +180,6 @@ use std::fmt;
<edit>
<path>src/shapes/circle.rs</path>
<description>Add a Display implementation for Circle</description>
<operation>insert_after</operation>
<old_text>
Circle { radius }

View File

@@ -388,6 +388,8 @@
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to show indent guides in the outline panel.
"indent_guides": true,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
@@ -777,6 +779,7 @@
"tasks": {
"variables": {}
},
"toolchain": { "name": "default", "path": "default" },
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
// use those languages.

View File

@@ -298,25 +298,64 @@ fn register_context_server_handlers(cx: &mut AppContext) {
return;
};
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
}
}
}
})
.detach();
}
},
);
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let tool_registry = ToolRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
for tool in tools.tools {
log::info!(
"registering context server tool: {:?}",
tool.name
);
context_server_registry.register_tool(
server.id.clone(),
tool.name.as_str(),
);
tool_registry.register_tool(
tools::context_server_tool::ContextServerTool::new(
server.id.clone(),
tool
),
);
}
}
}
})
@@ -334,6 +373,14 @@ fn register_context_server_handlers(cx: &mut AppContext) {
context_server_registry.unregister_command(&server_id, &command_name);
}
}
if let Some(tools) = context_server_registry.get_tools(server_id) {
let tool_registry = ToolRegistry::global(cx);
for tool_name in tools {
tool_registry.unregister_tool_by_name(&tool_name);
context_server_registry.unregister_tool(&server_id, &tool_name);
}
}
}
},
)

View File

@@ -1667,8 +1667,10 @@ impl ContextEditor {
});
}
fn cursors(&self, cx: &AppContext) -> Vec<usize> {
let selections = self.editor.read(cx).selections.all::<usize>(cx);
fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
let selections = self
.editor
.update(cx, |editor, cx| editor.selections.all::<usize>(cx));
selections
.into_iter()
.map(|selection| selection.head())
@@ -2964,7 +2966,7 @@ impl ContextEditor {
let mut creases = vec![];
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let selections = editor.selections.all::<Point>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
for selection in selections {
let range = editor::ToOffset::to_offset(&selection.start, &buffer)

View File

@@ -636,7 +636,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn one".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -690,7 +690,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -754,7 +754,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -798,7 +798,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,

View File

@@ -54,7 +54,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use ui::{prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use util::{RangeExt, ResultExt};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -189,11 +189,16 @@ impl InlineAssistant {
initial_prompt: Option<String>,
cx: &mut WindowContext,
) {
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.buffer().read(cx).snapshot(cx),
editor.selections.all::<Point>(cx),
)
});
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in editor.read(cx).selections.all::<Point>(cx) {
for mut selection in initial_selections {
if selection.end > selection.start {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
@@ -1596,7 +1601,7 @@ impl PromptEditor {
// always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), cx), cx);
editor
});
@@ -1653,6 +1658,7 @@ impl PromptEditor {
self.editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), cx), cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, cx);
if focus {
@@ -1663,6 +1669,20 @@ impl PromptEditor {
self.subscribe_to_editor(cx);
}
fn placeholder_text(codegen: &Codegen, cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
let action = if codegen.is_insertion {
"Generate"
} else {
"Transform"
};
format!("{action}{context_keybinding} • ↓↑ for history")
}
fn prompt(&self, cx: &AppContext) -> String {
self.editor.read(cx).text(cx)
}
@@ -2260,6 +2280,7 @@ pub struct Codegen {
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
}
impl Codegen {
@@ -2282,6 +2303,7 @@ impl Codegen {
)
});
let mut this = Self {
is_insertion: range.to_offset(&buffer.read(cx).snapshot(cx)).is_empty(),
alternatives: vec![codegen],
active_alternative: 0,
seen_alternatives: HashSet::default(),
@@ -2683,7 +2705,7 @@ impl CodegenAlternative {
let prompt = self
.builder
.generate_content_prompt(user_prompt, language_name, buffer, range)
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let mut messages = Vec::new();

View File

@@ -33,21 +33,21 @@ pub enum AssistantEditKind {
Update {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
Create {
new_text: String,
description: String,
description: Option<String>,
},
InsertBefore {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
InsertAfter {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
Delete {
old_text: String,
@@ -86,19 +86,37 @@ enum SearchDirection {
Diagonal,
}
// A measure of the currently quality of an in-progress fuzzy search.
//
// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
// operation in the search.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SearchState {
score: u32,
cost: u32,
direction: SearchDirection,
}
impl SearchState {
fn new(score: u32, direction: SearchDirection) -> Self {
Self { score, direction }
fn new(cost: u32, direction: SearchDirection) -> Self {
Self { cost, direction }
}
}
struct SearchMatrix {
cols: usize,
data: Vec<SearchState>,
}
impl SearchMatrix {
fn new(rows: usize, cols: usize) -> Self {
SearchMatrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
@@ -187,23 +205,23 @@ impl AssistantEdit {
"update" => AssistantEditKind::Update {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"insert_before" => AssistantEditKind::InsertBefore {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"insert_after" => AssistantEditKind::InsertAfter {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"delete" => AssistantEditKind::Delete {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
},
"create" => AssistantEditKind::Create {
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
},
_ => Err(anyhow!("unknown operation {operation:?}"))?,
@@ -264,7 +282,7 @@ impl AssistantEditKind {
ResolvedEdit {
range,
new_text,
description: Some(description),
description,
}
}
Self::Create {
@@ -272,7 +290,7 @@ impl AssistantEditKind {
description,
} => ResolvedEdit {
range: text::Anchor::MIN..text::Anchor::MAX,
description: Some(description),
description,
new_text,
},
Self::InsertBefore {
@@ -285,7 +303,7 @@ impl AssistantEditKind {
ResolvedEdit {
range: range.start..range.start,
new_text,
description: Some(description),
description,
}
}
Self::InsertAfter {
@@ -298,7 +316,7 @@ impl AssistantEditKind {
ResolvedEdit {
range: range.end..range.end,
new_text,
description: Some(description),
description,
}
}
Self::Delete { old_text } => {
@@ -314,44 +332,29 @@ impl AssistantEditKind {
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
const INSERTION_COST: u32 = 3;
const DELETION_COST: u32 = 10;
const WHITESPACE_INSERTION_COST: u32 = 1;
const DELETION_COST: u32 = 3;
const WHITESPACE_DELETION_COST: u32 = 1;
const EQUALITY_BONUS: u32 = 5;
struct Matrix {
cols: usize,
data: Vec<SearchState>,
}
impl Matrix {
fn new(rows: usize, cols: usize) -> Self {
Matrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
let buffer_len = buffer.len();
let query_len = search_query.len();
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
let mut leading_deletion_cost = 0_u32;
for (row, query_byte) in search_query.bytes().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
matrix.set(
row + 1,
0,
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
);
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
WHITESPACE_INSERTION_COST
} else {
@@ -359,38 +362,35 @@ impl AssistantEditKind {
};
let up = SearchState::new(
matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
SearchDirection::Up,
);
let left = SearchState::new(
matrix
.get(row + 1, col)
.score
.saturating_sub(insertion_cost),
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
SearchDirection::Left,
);
let diagonal = SearchState::new(
if query_byte == *buffer_byte {
matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
matrix.get(row, col).cost
} else {
matrix
.get(row, col)
.score
.saturating_sub(deletion_cost + insertion_cost)
.cost
.saturating_add(deletion_cost + insertion_cost)
},
SearchDirection::Diagonal,
);
matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
}
}
// Traceback to find the best match
let mut best_buffer_end = buffer_len;
let mut best_score = 0;
let mut best_cost = u32::MAX;
for col in 1..=buffer_len {
let score = matrix.get(query_len, col).score;
if score > best_score {
best_score = score;
let cost = matrix.get(query_len, col).cost;
if cost < best_cost {
best_cost = cost;
best_buffer_end = col;
}
}
@@ -560,89 +560,84 @@ mod tests {
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
};
use settings::SettingsStore;
use text::{OffsetRangeExt, Point};
use ui::BorrowAppContext;
use unindent::Unindent as _;
use util::test::{generate_marked_text, marked_text_ranges};
#[gpui::test]
fn test_resolve_location(cx: &mut AppContext) {
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
" Lorem\n",
" ipsum\n",
" dolor sit amet\n",
" consecteur",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
Point::new(1, 0)..Point::new(2, 18)
);
}
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum\n",
" dolor sit amet»\n",
" consecteur",
),
"ipsum\ndolor",
cx,
);
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
"fn foo1(a: usize) -> usize {\n",
" 40\n",
"}\n",
"\n",
"fn foo2(b: usize) -> usize {\n",
" 42\n",
"}\n",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
.to_point(&snapshot),
Point::new(0, 0)..Point::new(2, 1)
);
}
assert_location_resolution(
&"
«fn foo1(a: usize) -> usize {
40
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
"fn main() {\n",
" Foo\n",
" .bar()\n",
" .baz()\n",
" .qux()\n",
"}\n",
"\n",
"fn foo2(b: usize) -> usize {\n",
" 42\n",
"}\n",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
.to_point(&snapshot),
Point::new(1, 0)..Point::new(4, 14)
);
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"fn foo1(b: usize) {\n40\n}",
cx,
);
assert_location_resolution(
&"
fn main() {
« Foo
.bar()
.baz()
.qux()»
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"Foo.bar.baz.qux()",
cx,
);
assert_location_resolution(
&"
class Something {
one() { return 1; }
« two() { return 2222; }
three() { return 333; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
» seven() { return 7; }
eight() { return 8; }
}
"
.unindent(),
&"
two() { return 2222; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
"
.unindent(),
cx,
);
}
#[gpui::test]
fn test_resolve_edits(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
init_test(cx);
assert_edits(
"
@@ -675,7 +670,7 @@ mod tests {
last_name: String,
"
.unindent(),
description: "".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -690,7 +685,7 @@ mod tests {
}
"
.unindent(),
description: "".into(),
description: None,
},
],
"
@@ -734,7 +729,7 @@ mod tests {
qux();
}"
.unindent(),
description: "implement bar".into(),
description: Some("implement bar".into()),
},
AssistantEditKind::Update {
old_text: "
@@ -747,7 +742,7 @@ mod tests {
bar();
}"
.unindent(),
description: "call bar in foo".into(),
description: Some("call bar in foo".into()),
},
AssistantEditKind::InsertAfter {
old_text: "
@@ -762,7 +757,7 @@ mod tests {
}
"
.unindent(),
description: "implement qux".into(),
description: Some("implement qux".into()),
},
],
"
@@ -814,7 +809,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -829,7 +824,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -844,7 +839,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
],
"
@@ -865,6 +860,69 @@ mod tests {
.unindent(),
cx,
);
assert_edits(
"
impl Person {
fn set_name(&mut self, name: String) {
self.name = name;
}
fn name(&self) -> String {
return self.name;
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "self.name = name;".unindent(),
new_text: "self._name = name;".unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "return self.name;\n".unindent(),
new_text: "return self._name;\n".unindent(),
description: None,
},
],
"
impl Person {
fn set_name(&mut self, name: String) {
self._name = name;
}
fn name(&self) -> String {
return self._name;
}
}
"
.unindent(),
cx,
);
}
fn init_test(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
}
#[track_caller]
fn assert_location_resolution(
text_with_expected_range: &str,
query: &str,
cx: &mut AppContext,
) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
}
#[track_caller]

View File

@@ -204,7 +204,7 @@ impl PromptBuilder {
Ok(())
}
pub fn generate_content_prompt(
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,
language_name: Option<&LanguageName>,

View File

@@ -1 +1,2 @@
pub mod context_server_tool;
pub mod now_tool;

View File

@@ -0,0 +1,82 @@
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use context_servers::manager::ContextServerManager;
use context_servers::types;
use gpui::Task;
pub struct ContextServerTool {
server_id: String,
tool: types::Tool,
}
impl ContextServerTool {
pub fn new(server_id: impl Into<String>, tool: types::Tool) -> Self {
Self {
server_id: server_id.into(),
tool,
}
}
}
impl Tool for ContextServerTool {
fn name(&self) -> String {
self.tool.name.clone()
}
fn description(&self) -> String {
self.tool.description.clone().unwrap_or_default()
}
fn input_schema(&self) -> serde_json::Value {
match &self.tool.input_schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
}
serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] })
}
_ => self.tool.input_schema.clone(),
}
}
fn run(
self: std::sync::Arc<Self>,
input: serde_json::Value,
_workspace: gpui::WeakView<workspace::Workspace>,
cx: &mut ui::WindowContext,
) -> gpui::Task<gpui::Result<String>> {
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
if let Some(server) = manager.get_server(&self.server_id) {
cx.foreground_executor().spawn({
let tool_name = self.tool.name.clone();
async move {
let Some(protocol) = server.client.read().clone() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol.run_tool(tool_name, arguments).await?;
let tool_result = match response.tool_result {
serde_json::Value::String(s) => s,
_ => serde_json::to_string(&response.tool_result)?,
};
Ok(tool_result)
}
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}
}
}

View File

@@ -3,7 +3,7 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
@@ -33,30 +33,11 @@ struct NotesVersion {
version: clock::Global,
}
#[derive(Debug, Clone)]
pub struct HostedProject {
project_id: ProjectId,
channel_id: ChannelId,
name: SharedString,
_visibility: proto::ChannelVisibility,
}
impl From<proto::HostedProject> for HostedProject {
fn from(project: proto::HostedProject) -> Self {
Self {
project_id: ProjectId(project.project_id),
channel_id: ChannelId(project.channel_id),
_visibility: project.visibility(),
name: project.name.into(),
}
}
}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
hosted_projects: HashMap<ProjectId, HostedProject>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
@@ -85,7 +66,6 @@ pub struct ChannelState {
observed_notes_version: NotesVersion,
observed_chat_message: Option<u64>,
role: Option<ChannelRole>,
projects: HashSet<ProjectId>,
}
impl Channel {
@@ -216,7 +196,6 @@ impl ChannelStore {
channel_invitations: Vec::default(),
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
hosted_projects: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
opened_chats: Default::default(),
@@ -316,19 +295,6 @@ impl ChannelStore {
self.channel_index.by_id().get(&channel_id)
}
pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, ProjectId)> {
let mut projects: Vec<(SharedString, ProjectId)> = self
.channel_states
.get(&channel_id)
.map(|state| state.projects.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id)))
.collect();
projects.sort();
projects
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
@@ -1102,9 +1068,7 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
|| !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty()
|| !payload.hosted_projects.is_empty()
|| !payload.deleted_hosted_projects.is_empty();
|| !payload.latest_channel_buffer_versions.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -1161,34 +1125,6 @@ impl ChannelStore {
.or_default()
.update_latest_message_id(latest_channel_message.message_id);
}
for hosted_project in payload.hosted_projects {
let hosted_project: HostedProject = hosted_project.into();
if let Some(old_project) = self
.hosted_projects
.insert(hosted_project.project_id, hosted_project.clone())
{
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_hosted_project(old_project.project_id);
}
self.channel_states
.entry(hosted_project.channel_id)
.or_default()
.add_hosted_project(hosted_project.project_id);
}
for hosted_project_id in payload.deleted_hosted_projects {
let hosted_project_id = ProjectId(hosted_project_id);
if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_hosted_project(old_project.project_id);
}
}
}
cx.notify();
@@ -1295,12 +1231,4 @@ impl ChannelState {
};
}
}
fn add_hosted_project(&mut self, project_id: ProjectId) {
self.projects.insert(project_id);
}
fn remove_hosted_project(&mut self, project_id: ProjectId) {
self.projects.remove(&project_id);
}
}

View File

@@ -252,7 +252,10 @@ async fn create_billing_subscription(
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
let stripe_model = stripe_billing.register_model(default_model).await?;
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
let checkout_session_url = stripe_billing
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
.await?;

View File

@@ -617,7 +617,6 @@ pub struct ChannelsForUser {
pub channels: Vec<Channel>,
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub invited_channels: Vec<Channel>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,

View File

@@ -10,7 +10,6 @@ pub mod contacts;
pub mod contributors;
pub mod embeddings;
pub mod extensions;
pub mod hosted_projects;
pub mod messages;
pub mod notifications;
pub mod processed_stripe_events;

View File

@@ -615,15 +615,10 @@ impl Database {
.observed_channel_messages(&channel_ids, user_id, tx)
.await?;
let hosted_projects = self
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
.await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
invited_channels,
hosted_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,

View File

@@ -1,85 +0,0 @@
use rpc::{proto, ErrorCode};
use super::*;
impl Database {
pub async fn get_hosted_projects(
&self,
channel_ids: &[ChannelId],
roles: &HashMap<ChannelId, ChannelRole>,
tx: &DatabaseTransaction,
) -> Result<Vec<proto::HostedProject>> {
let projects = hosted_project::Entity::find()
.find_also_related(project::Entity)
.filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.all(tx)
.await?
.into_iter()
.flat_map(|(hosted_project, project)| {
if hosted_project.deleted_at.is_some() {
return None;
}
match hosted_project.visibility {
ChannelVisibility::Public => {}
ChannelVisibility::Members => {
let is_visible = roles
.get(&hosted_project.channel_id)
.map(|role| role.can_see_all_descendants())
.unwrap_or(false);
if !is_visible {
return None;
}
}
};
Some(proto::HostedProject {
project_id: project?.id.to_proto(),
channel_id: hosted_project.channel_id.to_proto(),
name: hosted_project.name.clone(),
visibility: hosted_project.visibility.into(),
})
})
.collect();
Ok(projects)
}
pub async fn get_hosted_project(
&self,
hosted_project_id: HostedProjectId,
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<(hosted_project::Model, ChannelRole)> {
let project = hosted_project::Entity::find_by_id(hosted_project_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
let channel = channel::Entity::find_by_id(project.channel_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
let role = match project.visibility {
ChannelVisibility::Public => {
self.check_user_is_channel_participant(&channel, user_id, tx)
.await?
}
ChannelVisibility::Members => {
self.check_user_is_channel_member(&channel, user_id, tx)
.await?
}
};
Ok((project, role))
}
pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result<bool> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.map(|project| project.hosted_project_id.is_some())
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
})
.await
}
}

View File

@@ -68,7 +68,6 @@ impl Database {
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -536,39 +535,6 @@ impl Database {
.await
}
/// Adds the given connection to the specified hosted project
pub async fn join_hosted_project(
&self,
id: ProjectId,
user_id: UserId,
connection: ConnectionId,
) -> Result<(Project, ReplicaId)> {
self.transaction(|tx| async move {
let (project, hosted_project) = project::Entity::find_by_id(id)
.find_also_related(hosted_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
let Some(hosted_project) = hosted_project else {
return Err(anyhow!("project is not hosted"))?;
};
let channel = channel::Entity::find_by_id(hosted_project.channel_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
let role = self
.check_user_is_channel_participant(&channel, user_id, &tx)
.await?;
self.join_project_internal(project, user_id, connection, role, &tx)
.await
})
.await
}
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(id)

View File

@@ -18,7 +18,6 @@ pub mod extension;
pub mod extension_version;
pub mod feature_flag;
pub mod follower;
pub mod hosted_project;
pub mod language_server;
pub mod notification;
pub mod notification_kind;

View File

@@ -1,27 +0,0 @@
use crate::db::{ChannelId, ChannelVisibility, HostedProjectId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "hosted_projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: HostedProjectId,
pub channel_id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
pub deleted_at: Option<DateTime>,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::project::Entity")]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}

View File

@@ -1,4 +1,4 @@
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -12,7 +12,6 @@ pub struct Model {
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
pub hosted_project_id: Option<HostedProjectId>,
}
impl Model {
@@ -50,12 +49,6 @@ pub enum Relation {
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
#[sea_orm(
belongs_to = "super::hosted_project::Entity",
from = "Column::HostedProjectId",
to = "super::hosted_project::Column::Id"
)]
HostedProject,
}
impl Related<super::user::Entity> for Entity {
@@ -88,10 +81,4 @@ impl Related<super::language_server::Entity> for Entity {
}
}
impl Related<super::hosted_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostedProject.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -449,6 +449,10 @@ async fn check_usage_limit(
model_name: &str,
claims: &LlmTokenClaims,
) -> Result<()> {
if claims.is_staff {
return Ok(());
}
let model = state.db.model(provider, model_name)?;
let usage = state
.db
@@ -513,11 +517,6 @@ async fn check_usage_limit(
];
for (used, limit, usage_measure) in checks {
// Temporarily bypass rate-limiting for staff members.
if claims.is_staff {
continue;
}
if used > limit {
let resource = match usage_measure {
UsageMeasure::RequestsPerMinute => "requests_per_minute",

View File

@@ -287,7 +287,6 @@ impl Server {
.add_request_handler(share_project)
.add_message_handler(unshare_project)
.add_request_handler(join_project)
.add_request_handler(join_hosted_project)
.add_message_handler(leave_project)
.add_request_handler(update_project)
.add_request_handler(update_worktree)
@@ -308,6 +307,8 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
.add_request_handler(
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
@@ -1793,11 +1794,6 @@ impl JoinProjectInternalResponse for Response<proto::JoinProject> {
Response::<proto::JoinProject>::send(self, result)
}
}
impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
Response::<proto::JoinHostedProject>::send(self, result)
}
}
fn join_project_internal(
response: impl JoinProjectInternalResponse,
@@ -1921,11 +1917,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id);
let db = session.db().await;
if db.is_hosted_project(project_id).await? {
let project = db.leave_hosted_project(project_id, sender_id).await?;
project_left(&project, &session);
return Ok(());
}
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
tracing::info!(
@@ -1941,24 +1932,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
Ok(())
}
async fn join_hosted_project(
request: proto::JoinHostedProject,
response: Response<proto::JoinHostedProject>,
session: Session,
) -> Result<()> {
let (mut project, replica_id) = session
.db()
.await
.join_hosted_project(
ProjectId(request.project_id as i32),
session.user_id(),
session.connection_id,
)
.await?;
join_project_internal(response, session, &mut project, &replica_id)
}
/// Updates other participants with changes to the project
async fn update_project(
request: proto::UpdateProject,
@@ -4200,7 +4173,6 @@ fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
update.channel_invitations.push(channel.to_proto());
}
update.hosted_projects = channels.hosted_projects;
update
}

View File

@@ -1957,9 +1957,10 @@ async fn test_following_to_channel_notes_without_a_shared_project(
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
notes.editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
})
});
// Client A opens the notes for channel 2.

View File

@@ -6575,3 +6575,95 @@ async fn test_context_collaboration_with_reconnect(
assert!(context.buffer().read(cx).read_only());
});
}
#[gpui::test]
async fn test_remote_git_branches(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).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);
client_a
.fs()
.insert_tree("/project", serde_json::json!({ ".git":{} }))
.await;
let branches = ["main", "dev", "feature-1"];
client_a
.fs()
.insert_branches(Path::new("/project/.git"), &branches);
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let root_path = ProjectPath::root_path(worktree_id);
// Client A sees that a guest has joined.
executor.run_until_parked();
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
let new_branch = branches[2];
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
assert_eq!(host_branch.as_ref(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});
assert_eq!(host_branch.as_ref(), "totally-new-branch");
}

View File

@@ -1,7 +1,7 @@
use crate::tests::TestServer;
use call::ActiveCall;
use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext};
use gpui::{BackgroundExecutor, Context as _, TestAppContext};
use http_client::BlockedHttpClient;
use language::{language_settings::language_settings, LanguageRegistry};
use node_runtime::NodeRuntime;
@@ -174,3 +174,133 @@ async fn test_sharing_an_ssh_remote_project(
);
});
}
#[gpui::test]
async fn test_ssh_collaboration_git_branches(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx_a.set_name("a");
cx_b.set_name("b");
server_cx.set_name("server");
let mut server = TestServer::start(executor.clone()).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;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
.await;
let branches = ["main", "dev", "feature-1"];
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let headless_project = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: node,
languages,
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// User B joins the project.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Give client A sometime to see that B has joined, and that the headless server
// has some git repositories
executor.run_until_parked();
let root_path = ProjectPath::root_path(worktree_id);
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
let new_branch = branches[2];
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
assert_eq!(server_branch.as_ref(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});
assert_eq!(server_branch.as_ref(), "totally-new-branch");
}

View File

@@ -5,7 +5,7 @@ use self::channel_modal::ChannelModal;
use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
use client::{ChannelId, Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
@@ -182,10 +182,6 @@ enum ListEntry {
ChannelEditor {
depth: usize,
},
HostedProject {
id: ProjectId,
name: SharedString,
},
Contact {
contact: Arc<Contact>,
calling: bool,
@@ -566,7 +562,6 @@ impl CollabPanel {
}
}
let hosted_projects = channel_store.projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@@ -600,10 +595,6 @@ impl CollabPanel {
});
}
}
for (name, id) in hosted_projects {
self.entries.push(ListEntry::HostedProject { id, name });
}
}
}
@@ -1029,40 +1020,6 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
}
fn render_channel_project(
&self,
id: ProjectId,
name: &SharedString,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
ListItem::new(ElementId::NamedInteger(
"channel-project".into(),
id.0 as usize,
))
.indent_level(2)
.indent_step_size(px(20.))
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err(
"Failed to open project",
cx,
|_, _| None,
)
}
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(IconButton::new(0, IconName::FileTree)),
)
.child(Label::new(name.clone()))
.tooltip(move |cx| Tooltip::text("Open Project", cx))
}
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@@ -1538,12 +1495,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => {
self.join_channel_chat(*channel_id, cx)
}
ListEntry::HostedProject {
id: _id,
name: _name,
} => {
// todo()
}
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
}
@@ -2157,10 +2108,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => self
.render_channel_chat(*channel_id, is_selected, cx)
.into_any_element(),
ListEntry::HostedProject { id, name } => self
.render_channel_project(*id, name, is_selected, cx)
.into_any_element(),
}
}
@@ -2898,11 +2845,6 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id;
}
}
ListEntry::HostedProject { id, .. } => {
if let ListEntry::HostedProject { id: other_id, .. } = other {
return id == other_id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,

View File

@@ -180,6 +180,39 @@ impl InitializedContextServerProtocol {
Ok(completion)
}
/// List MCP tools.
pub async fn list_tools(&self) -> Result<types::ListToolsResponse> {
self.check_capability(ServerCapability::Tools)?;
let response = self
.inner
.request::<types::ListToolsResponse>(types::RequestType::ListTools.as_str(), ())
.await?;
Ok(response)
}
/// Executes a tool with the given arguments
pub async fn run_tool<P: AsRef<str>>(
&self,
tool: P,
arguments: Option<HashMap<String, serde_json::Value>>,
) -> Result<types::CallToolResponse> {
self.check_capability(ServerCapability::Tools)?;
let params = types::CallToolParams {
name: tool.as_ref().to_string(),
arguments,
};
let response: types::CallToolResponse = self
.inner
.request(types::RequestType::CallTool.as_str(), params)
.await?;
Ok(response)
}
}
impl InitializedContextServerProtocol {

View File

@@ -9,7 +9,8 @@ struct GlobalContextServerRegistry(Arc<ContextServerRegistry>);
impl Global for GlobalContextServerRegistry {}
pub struct ContextServerRegistry {
registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
command_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
tool_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
}
impl ContextServerRegistry {
@@ -20,13 +21,14 @@ impl ContextServerRegistry {
pub fn register(cx: &mut AppContext) {
cx.set_global(GlobalContextServerRegistry(Arc::new(
ContextServerRegistry {
registry: RwLock::new(HashMap::default()),
command_registry: RwLock::new(HashMap::default()),
tool_registry: RwLock::new(HashMap::default()),
},
)))
}
pub fn register_command(&self, server_id: String, command_name: &str) {
let mut registry = self.registry.write();
let mut registry = self.command_registry.write();
registry
.entry(server_id)
.or_default()
@@ -34,14 +36,34 @@ impl ContextServerRegistry {
}
pub fn unregister_command(&self, server_id: &str, command_name: &str) {
let mut registry = self.registry.write();
let mut registry = self.command_registry.write();
if let Some(commands) = registry.get_mut(server_id) {
commands.retain(|name| name.as_ref() != command_name);
}
}
pub fn get_commands(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.registry.read();
let registry = self.command_registry.read();
registry.get(server_id).cloned()
}
pub fn register_tool(&self, server_id: String, tool_name: &str) {
let mut registry = self.tool_registry.write();
registry
.entry(server_id)
.or_default()
.push(tool_name.into());
}
pub fn unregister_tool(&self, server_id: &str, tool_name: &str) {
let mut registry = self.tool_registry.write();
if let Some(tools) = registry.get_mut(server_id) {
tools.retain(|name| name.as_ref() != tool_name);
}
}
pub fn get_tools(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.tool_registry.read();
registry.get(server_id).cloned()
}
}

View File

@@ -16,6 +16,8 @@ pub enum RequestType {
PromptsList,
CompletionComplete,
Ping,
ListTools,
ListResourceTemplates,
}
impl RequestType {
@@ -32,6 +34,8 @@ impl RequestType {
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
RequestType::Ping => "ping",
RequestType::ListTools => "tools/list",
RequestType::ListResourceTemplates => "resources/templates/list",
}
}
}
@@ -402,3 +406,17 @@ pub struct Completion {
pub values: Vec<String>,
pub total: CompletionTotal,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
pub tool_result: serde_json::Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResponse {
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}

View File

@@ -81,7 +81,6 @@ ui.workspace = true
url.workspace = true
util.workspace = true
workspace.workspace = true
unicode-segmentation.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -153,6 +153,10 @@ pub struct DeleteToPreviousWordStart {
pub ignore_newlines: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAtLevel {
pub level: u32,
}
impl_actions!(
editor,
[
@@ -182,6 +186,7 @@ impl_actions!(
ToggleCodeActions,
ToggleComments,
UnfoldAt,
FoldAtLevel
]
);
@@ -193,6 +198,7 @@ gpui::actions!(
AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,
ApplyAllDiffHunks,
ApplyDiffHunk,
Backspace,
Cancel,

View File

@@ -8,7 +8,7 @@
//! of several smaller structures that form a hierarchy (starting at the bottom):
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
//! - [`CharMap`] that replaces tabs and non-printable characters
//! - [`TabMap`] that keeps track of hard tabs in a buffer.
//! - [`WrapMap`] that handles soft wrapping.
//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
//! - [`DisplayMap`] that adds background highlights to the regions of text.
@@ -18,11 +18,10 @@
//! [EditorElement]: crate::element::EditorElement
mod block_map;
mod char_map;
mod crease_map;
mod fold_map;
mod inlay_map;
mod invisibles;
mod tab_map;
mod wrap_map;
use crate::{
@@ -33,7 +32,6 @@ pub use block_map::{
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use char_map::{CharMap, CharSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
@@ -44,7 +42,6 @@ use gpui::{
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::is_invisible;
use language::{
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,
@@ -64,9 +61,9 @@ use std::{
sync::Arc,
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use ui::{px, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use ui::WindowContext;
use wrap_map::{WrapMap, WrapSnapshot};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -97,7 +94,7 @@ pub struct DisplayMap {
/// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
fold_map: FoldMap,
/// Keeps track of hard tabs in a buffer.
char_map: CharMap,
tab_map: TabMap,
/// Handles soft wrapping.
wrap_map: Model<WrapMap>,
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
@@ -134,7 +131,7 @@ impl DisplayMap {
let crease_map = CreaseMap::new(&buffer_snapshot);
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (char_map, snapshot) = CharMap::new(snapshot, tab_size);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(
snapshot,
@@ -151,7 +148,7 @@ impl DisplayMap {
buffer_subscription,
fold_map,
inlay_map,
char_map,
tab_map,
wrap_map,
block_map,
crease_map,
@@ -169,17 +166,17 @@ impl DisplayMap {
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (char_snapshot, edits) = self.char_map.sync(fold_snapshot.clone(), edits, tab_size);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(char_snapshot.clone(), edits, cx));
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
inlay_snapshot,
char_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
crease_snapshot: self.crease_map.snapshot(),
@@ -215,13 +212,13 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -239,13 +236,13 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -280,7 +277,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -298,7 +295,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -316,7 +313,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -334,7 +331,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -410,7 +407,7 @@ impl DisplayMap {
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -418,7 +415,7 @@ impl DisplayMap {
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
@@ -470,7 +467,7 @@ pub struct DisplaySnapshot {
pub fold_snapshot: FoldSnapshot,
pub crease_snapshot: CreaseSnapshot,
inlay_snapshot: InlaySnapshot,
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
wrap_snapshot: WrapSnapshot,
block_snapshot: BlockSnapshot,
text_highlights: TextHighlights,
@@ -570,8 +567,8 @@ impl DisplaySnapshot {
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let char_point = self.char_snapshot.to_char_point(fold_point);
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
@@ -599,21 +596,21 @@ impl DisplaySnapshot {
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
let fold_point = self.char_snapshot.to_fold_point(char_point, bias).0;
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_inlay_point(&self.fold_snapshot)
}
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
self.char_snapshot.to_fold_point(char_point, bias).0
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
self.tab_snapshot.to_fold_point(tab_point, bias).0
}
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
let char_point = self.char_snapshot.to_char_point(fold_point);
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
@@ -691,23 +688,6 @@ impl DisplaySnapshot {
}
}
if chunk.is_invisible {
let invisible_highlight = HighlightStyle {
background_color: Some(editor_style.status.hint_background),
underline: Some(UnderlineStyle {
color: Some(editor_style.status.hint),
thickness: px(1.),
wavy: false,
}),
..Default::default()
};
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(invisible_highlight);
} else {
highlight_style = Some(invisible_highlight);
}
}
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
@@ -804,11 +784,12 @@ impl DisplaySnapshot {
layout_line.closest_index_for_x(x) as u32
}
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<String> {
pub fn display_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
let chars = self
.text_chunks(point.row())
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
let mut column = 0;
@@ -818,21 +799,16 @@ impl DisplaySnapshot {
!at_point
}
})
.take_while({
let mut prev = false;
move |char| {
let now = char.is_ascii();
let end = char.is_ascii() && (char.is_ascii_whitespace() || prev);
prev = now;
!end
.map(move |ch| {
let result = (ch, point);
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
});
chars
.collect::<String>()
.graphemes(true)
.next()
.map(|s| s.to_owned())
result
})
}
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
@@ -1144,8 +1120,8 @@ impl DisplayPoint {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let char_point = map.wrap_snapshot.to_char_point(wrap_point);
let fold_point = map.char_snapshot.to_fold_point(char_point, bias).0;
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
map.inlay_snapshot
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
@@ -1192,6 +1168,7 @@ pub mod tests {
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::{LoadThemes, SyntaxTheme};
use unindent::Unindent as _;
use util::test::{marked_text_ranges, sample_text};
use Bias::*;
@@ -1253,7 +1230,7 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("char text: {:?}", snapshot.char_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
@@ -1368,7 +1345,7 @@ pub mod tests {
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("char text: {:?}", snapshot.char_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
@@ -1648,8 +1625,6 @@ pub mod tests {
#[gpui::test]
async fn test_chunks(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _;
let text = r#"
fn outer() {}
@@ -1746,12 +1721,110 @@ pub mod tests {
);
}
#[gpui::test]
async fn test_chunks_with_syntax_highlighting_across_blocks(cx: &mut gpui::TestAppContext) {
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);
let text = r#"
const A: &str = "
one
two
three
";
const B: &str = "four";
"#
.unindent();
let theme = SyntaxTheme::new_test(vec![
("string", Hsla::red()),
("punctuation", Hsla::blue()),
("keyword", Hsla::green()),
]);
let language = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_highlights_query(
r#"
(string_literal) @string
"const" @keyword
[":" ";"] @punctuation
"#,
)
.unwrap(),
);
language.set_theme(&theme);
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
// Insert a block in the middle of a multi-line string literal
map.update(cx, |map, cx| {
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Below(
buffer_snapshot.anchor_before(Point::new(1, 0)),
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
priority: 0,
}],
cx,
)
});
pretty_assertions::assert_eq!(
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(7), &map, &theme, cx)),
[
("const".into(), Some(Hsla::green())),
(" A".into(), None),
(":".into(), Some(Hsla::blue())),
(" &str = ".into(), None),
("\"\n one\n".into(), Some(Hsla::red())),
("\n".into(), None),
(" two\n three\n\"".into(), Some(Hsla::red())),
(";".into(), Some(Hsla::blue())),
("\n".into(), None),
("const".into(), Some(Hsla::green())),
(" B".into(), None),
(":".into(), Some(Hsla::blue())),
(" &str = ".into(), None),
("\"four\"".into(), Some(Hsla::red())),
(";".into(), Some(Hsla::blue())),
("\n".into(), None),
]
);
}
// todo(linux) fails due to pixel differences in text rendering
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _;
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);

View File

@@ -1666,7 +1666,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
mod tests {
use super::*;
use crate::display_map::{
char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap, wrap_map::WrapMap,
fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
};
use gpui::{div, font, px, AppContext, Context as _, Element};
use language::{Buffer, Capability};
@@ -1701,9 +1701,9 @@ mod tests {
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1851,10 +1851,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
@@ -1914,9 +1914,8 @@ mod tests {
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) =
WrapMap::new(char_snapshot, font, font_size, Some(wrap_width), cx);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let snapshot = block_map.read(wraps_snapshot, Default::default());
@@ -1953,9 +1952,9 @@ mod tests {
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -2055,15 +2054,9 @@ mod tests {
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = cx.update(|cx| {
WrapMap::new(
char_snapshot,
font("Helvetica"),
px(14.0),
Some(px(60.)),
cx,
)
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
});
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
@@ -2106,7 +2099,7 @@ mod tests {
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let tab_size = 1.try_into().unwrap();
let (mut tab_map, tab_snapshot) = CharMap::new(fold_snapshot, tab_size);
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
@@ -2257,9 +2250,9 @@ mod tests {
let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) = cx
.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), font_size, wrap_width, cx));
.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
@@ -2321,10 +2314,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.insert(block_properties.iter().map(|props| BlockProperties {
@@ -2346,10 +2339,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
@@ -2369,9 +2362,9 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(
@@ -2486,7 +2479,7 @@ mod tests {
.row as usize];
let soft_wrapped = wraps_snapshot
.to_char_point(WrapPoint::new(wrap_row, 0))
.to_tab_point(WrapPoint::new(wrap_row, 0))
.column()
> 0;
expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });

View File

@@ -1,157 +0,0 @@
use std::sync::LazyLock;
use collections::HashMap;
// Invisibility in a Unicode context is not well defined, so we have to guess.
//
// We highlight all ASCII control codes, and unicode whitespace because they are likely
// confused with a normal space (U+0020).
//
// We also highlight the handful of blank non-space characters:
// U+2800 BRAILLE PATTERN BLANK - Category: So
// U+115F HANGUL CHOSEONG FILLER - Category: Lo
// U+1160 HANGUL CHOSEONG FILLER - Category: Lo
// U+3164 HANGUL FILLER - Category: Lo
// U+FFA0 HALFWIDTH HANGUL FILLER - Category: Lo
// U+FFFC OBJECT REPLACEMENT CHARACTER - Category: So
//
// For the rest of Unicode, invisibility happens for two reasons:
// * A Format character (like a byte order mark or right-to-left override)
// * An invisible Nonspacing Mark character (like U+034F, or variation selectors)
//
// We don't consider unassigned codepoints invisible as the font renderer already shows
// a replacement character in that case (and there are a *lot* of them)
//
// Control characters are mostly fine to highlight; except:
// * U+E0020..=U+E007F are used in emoji flags. We don't highlight them right now, but we could if we tightened our heuristics.
// * U+200D is used to join characters. We highlight this but don't replace it. As our font system ignores mid-glyph highlights this mostly works to highlight unexpected uses.
//
// Nonspacing marks are handled like U+200D. This means that mid-glyph we ignore them, but
// probably causes issues with end-of-glyph usage.
//
// ref: https://invisible-characters.com
// ref: https://www.compart.com/en/unicode/category/Cf
// ref: https://gist.github.com/ConradIrwin/f759e1fc29267143c4c7895aa495dca5?h=1
// ref: https://unicode.org/Public/emoji/13.0/emoji-test.txt
// https://github.com/bits/UTF-8-Unicode-Test-Documents/blob/master/UTF-8_sequence_separated/utf8_sequence_0-0x10ffff_assigned_including-unprintable-asis.txt
pub fn is_invisible(c: char) -> bool {
if c <= '\u{1f}' {
c != '\t' && c != '\n' && c != '\r'
} else if c >= '\u{7f}' {
c <= '\u{9f}' || c.is_whitespace() || contains(c, &FORMAT) || contains(c, &OTHER)
} else {
false
}
}
pub(crate) fn replacement(c: char) -> Option<&'static str> {
if !is_invisible(c) {
return None;
}
if c <= '\x7f' {
REPLACEMENTS.get(&c).copied()
} else if contains(c, &PRESERVE) {
None
} else {
Some(" ")
}
}
const REPLACEMENTS: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
[
('\x00', ""),
('\x01', ""),
('\x02', ""),
('\x03', ""),
('\x04', ""),
('\x05', ""),
('\x06', ""),
('\x07', ""),
('\x08', ""),
('\x0B', ""),
('\x0C', ""),
('\x0D', ""),
('\x0E', ""),
('\x0F', ""),
('\x10', ""),
('\x11', ""),
('\x12', ""),
('\x13', ""),
('\x14', ""),
('\x15', ""),
('\x16', ""),
('\x17', ""),
('\x18', ""),
('\x19', ""),
('\x1A', ""),
('\x1B', ""),
('\x1C', ""),
('\x1D', ""),
('\x1E', ""),
('\x1F', ""),
('\u{007F}', ""),
]
.into_iter()
.collect()
});
// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
pub const FORMAT: &'static [(char, char)] = &[
('\u{ad}', '\u{ad}'),
('\u{600}', '\u{605}'),
('\u{61c}', '\u{61c}'),
('\u{6dd}', '\u{6dd}'),
('\u{70f}', '\u{70f}'),
('\u{890}', '\u{891}'),
('\u{8e2}', '\u{8e2}'),
('\u{180e}', '\u{180e}'),
('\u{200b}', '\u{200f}'),
('\u{202a}', '\u{202e}'),
('\u{2060}', '\u{2064}'),
('\u{2066}', '\u{206f}'),
('\u{feff}', '\u{feff}'),
('\u{fff9}', '\u{fffb}'),
('\u{110bd}', '\u{110bd}'),
('\u{110cd}', '\u{110cd}'),
('\u{13430}', '\u{1343f}'),
('\u{1bca0}', '\u{1bca3}'),
('\u{1d173}', '\u{1d17a}'),
('\u{e0001}', '\u{e0001}'),
('\u{e0020}', '\u{e007f}'),
];
// hand-made base on https://invisible-characters.com (Excluding Cf)
pub const OTHER: &'static [(char, char)] = &[
('\u{034f}', '\u{034f}'),
('\u{115F}', '\u{1160}'),
('\u{17b4}', '\u{17b5}'),
('\u{180b}', '\u{180d}'),
('\u{2800}', '\u{2800}'),
('\u{3164}', '\u{3164}'),
('\u{fe00}', '\u{fe0d}'),
('\u{ffa0}', '\u{ffa0}'),
('\u{fffc}', '\u{fffc}'),
('\u{e0100}', '\u{e01ef}'),
];
// a subset of FORMAT/OTHER that may appear within glyphs
const PRESERVE: &'static [(char, char)] = &[
('\u{034f}', '\u{034f}'),
('\u{200d}', '\u{200d}'),
('\u{17b4}', '\u{17b5}'),
('\u{180b}', '\u{180d}'),
('\u{e0061}', '\u{e007a}'),
('\u{e007f}', '\u{e007f}'),
];
fn contains(c: char, list: &[(char, char)]) -> bool {
for (start, end) in list {
if c < *start {
return false;
}
if c <= *end {
return true;
}
}
false
}

View File

@@ -1,6 +1,5 @@
use super::{
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
invisibles::{is_invisible, replacement},
Highlights,
};
use language::{Chunk, Point};
@@ -10,14 +9,14 @@ use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
/// Keeps track of hard tabs and non-printable characters in a text buffer.
/// Keeps track of hard tabs in a text buffer.
///
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub struct CharMap(CharSnapshot);
pub struct TabMap(TabSnapshot);
impl CharMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, CharSnapshot) {
let snapshot = CharSnapshot {
impl TabMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
@@ -27,7 +26,7 @@ impl CharMap {
}
#[cfg(test)]
pub fn set_max_expansion_column(&mut self, column: u32) -> CharSnapshot {
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
self.0.max_expansion_column = column;
self.0.clone()
}
@@ -37,9 +36,9 @@ impl CharMap {
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (CharSnapshot, Vec<TabEdit>) {
) -> (TabSnapshot, Vec<TabEdit>) {
let old_snapshot = &mut self.0;
let mut new_snapshot = CharSnapshot {
let mut new_snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
@@ -138,15 +137,15 @@ impl CharMap {
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_char_point(old_start)..old_snapshot.to_char_point(old_end),
new: new_snapshot.to_char_point(new_start)..new_snapshot.to_char_point(new_end),
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
});
}
} else {
new_snapshot.version += 1;
tab_edits.push(TabEdit {
old: CharPoint::zero()..old_snapshot.max_point(),
new: CharPoint::zero()..new_snapshot.max_point(),
old: TabPoint::zero()..old_snapshot.max_point(),
new: TabPoint::zero()..new_snapshot.max_point(),
});
}
@@ -156,14 +155,14 @@ impl CharMap {
}
#[derive(Clone)]
pub struct CharSnapshot {
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
}
impl CharSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
}
@@ -171,7 +170,7 @@ impl CharSnapshot {
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
self.to_char_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
.0
.column
} else {
@@ -180,10 +179,10 @@ impl CharSnapshot {
}
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(CharPoint::zero()..self.max_point())
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
pub fn text_summary_for_range(&self, range: Range<CharPoint>) -> TextSummary {
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
let input_start = self.to_fold_point(range.start, Bias::Left).0;
let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
@@ -212,7 +211,7 @@ impl CharSnapshot {
} else {
for _ in self
.chunks(
CharPoint::new(range.end.row(), 0)..range.end,
TabPoint::new(range.end.row(), 0)..range.end,
false,
Highlights::default(),
)
@@ -233,7 +232,7 @@ impl CharSnapshot {
pub fn chunks<'a>(
&'a self,
range: Range<CharPoint>,
range: Range<TabPoint>,
language_aware: bool,
highlights: Highlights<'a>,
) -> TabChunks<'a> {
@@ -280,7 +279,7 @@ impl CharSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(
CharPoint::zero()..self.max_point(),
TabPoint::zero()..self.max_point(),
false,
Highlights::default(),
)
@@ -288,24 +287,24 @@ impl CharSnapshot {
.collect()
}
pub fn max_point(&self) -> CharPoint {
self.to_char_point(self.fold_snapshot.max_point())
pub fn max_point(&self) -> TabPoint {
self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: CharPoint, bias: Bias) -> CharPoint {
self.to_char_point(
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
self.fold_snapshot
.clip_point(self.to_fold_point(point, bias).0, bias),
)
}
pub fn to_char_point(&self, input: FoldPoint) -> CharPoint {
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
CharPoint::new(input.row(), expanded)
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: CharPoint, bias: Bias) -> (FoldPoint, u32, u32) {
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
@@ -317,13 +316,13 @@ impl CharSnapshot {
)
}
pub fn make_char_point(&self, point: Point, bias: Bias) -> CharPoint {
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.to_char_point(fold_point)
self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: CharPoint, bias: Bias) -> Point {
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
let fold_point = self.to_fold_point(point, bias).0;
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
self.fold_snapshot
@@ -346,9 +345,6 @@ impl CharSnapshot {
let tab_len = tab_size - expanded_chars % tab_size;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else if let Some(replacement) = replacement(c) {
expanded_chars += replacement.chars().count() as u32;
expanded_bytes += replacement.len() as u32;
} else {
expanded_bytes += c.len_utf8() as u32;
expanded_chars += 1;
@@ -388,9 +384,6 @@ impl CharSnapshot {
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
};
}
} else if let Some(replacement) = replacement(c) {
expanded_chars += replacement.chars().count() as u32;
expanded_bytes += replacement.len() as u32;
} else {
expanded_chars += 1;
expanded_bytes += c.len_utf8() as u32;
@@ -412,9 +405,9 @@ impl CharSnapshot {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct CharPoint(pub Point);
pub struct TabPoint(pub Point);
impl CharPoint {
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
@@ -432,13 +425,13 @@ impl CharPoint {
}
}
impl From<Point> for CharPoint {
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
pub type TabEdit = text::Edit<CharPoint>;
pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
@@ -493,7 +486,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
snapshot: &'a CharSnapshot,
snapshot: &'a TabSnapshot,
fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
@@ -506,7 +499,7 @@ pub struct TabChunks<'a> {
}
impl<'a> TabChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<CharPoint>) {
pub(crate) fn seek(&mut self, range: Range<TabPoint>) {
let (input_start, expanded_char_column, to_next_stop) =
self.snapshot.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
@@ -591,37 +584,6 @@ impl<'a> Iterator for TabChunks<'a> {
self.input_column = 0;
self.output_position += Point::new(1, 0);
}
_ if is_invisible(c) => {
if ix > 0 {
let (prefix, suffix) = self.chunk.text.split_at(ix);
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
is_invisible: false,
..self.chunk.clone()
});
}
let c_len = c.len_utf8();
let replacement = replacement(c).unwrap_or(&self.chunk.text[..c_len]);
if self.chunk.text.len() >= c_len {
self.chunk.text = &self.chunk.text[c_len..];
} else {
self.chunk.text = "";
}
let len = replacement.chars().count() as u32;
let next_output_position = cmp::min(
self.output_position + Point::new(0, len),
self.max_output_position,
);
self.column += len;
self.input_column += 1;
self.output_position = next_output_position;
return Some(Chunk {
text: replacement,
is_invisible: true,
..self.chunk.clone()
});
}
_ => {
self.column += 1;
if !self.inside_leading_tab {
@@ -651,11 +613,11 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(char_snapshot.expand_tabs("\ta".chars(), 2), 5);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
}
#[gpui::test]
@@ -668,16 +630,16 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
char_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(char_snapshot.text(), output);
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
for (ix, c) in input.char_indices() {
assert_eq!(
char_snapshot
tab_snapshot
.chunks(
CharPoint::new(0, ix as u32)..char_snapshot.max_point(),
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
false,
Highlights::default(),
)
@@ -691,13 +653,13 @@ mod tests {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
char_snapshot.to_char_point(FoldPoint(input_point)),
CharPoint(output_point),
"to_char_point({input_point:?})"
tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
char_snapshot
.to_fold_point(CharPoint(output_point), Bias::Left)
tab_snapshot
.to_fold_point(TabPoint(output_point), Bias::Left)
.0,
FoldPoint(input_point),
"to_fold_point({output_point:?})"
@@ -715,10 +677,10 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
char_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(char_snapshot.text(), input);
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
@@ -729,10 +691,10 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&char_snapshot, CharPoint::zero()),
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
@@ -741,7 +703,7 @@ mod tests {
]
);
assert_eq!(
chunks(&char_snapshot, CharPoint::new(0, 2)),
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
@@ -750,7 +712,7 @@ mod tests {
]
);
fn chunks(snapshot: &CharSnapshot, start: CharPoint) -> Vec<(String, bool)> {
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
@@ -796,12 +758,12 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = char_map.set_max_expansion_column(32);
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"CharMap text (tab size: {}): {:?}",
"TabMap text (tab size: {}): {:?}",
tab_size,
tabs_snapshot.text(),
);
@@ -809,11 +771,11 @@ mod tests {
for _ in 0..5 {
let end_row = rng.gen_range(0..=text.max_point().row);
let end_column = rng.gen_range(0..=text.line_len(end_row));
let mut end = CharPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let start_row = rng.gen_range(0..=text.max_point().row);
let start_column = rng.gen_range(0..=text.line_len(start_row));
let mut start =
CharPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {
mem::swap(&mut start, &mut end);
}

View File

@@ -1,6 +1,6 @@
use super::{
char_map::{self, CharPoint, CharSnapshot, TabEdit},
fold_map::FoldBufferRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
@@ -12,7 +12,7 @@ use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, SumTree};
use text::Patch;
pub use super::char_map::TextSummary;
pub use super::tab_map::TextSummary;
pub type WrapEdit = text::Edit<u32>;
/// Handles soft wrapping of text.
@@ -20,7 +20,7 @@ pub type WrapEdit = text::Edit<u32>;
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub struct WrapMap {
snapshot: WrapSnapshot,
pending_edits: VecDeque<(CharSnapshot, Vec<TabEdit>)>,
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
wrap_width: Option<Pixels>,
@@ -30,7 +30,7 @@ pub struct WrapMap {
#[derive(Clone)]
pub struct WrapSnapshot {
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
transforms: SumTree<Transform>,
interpolated: bool,
}
@@ -51,11 +51,11 @@ struct TransformSummary {
pub struct WrapPoint(pub Point);
pub struct WrapChunks<'a> {
input_chunks: char_map::TabChunks<'a>,
input_chunks: tab_map::TabChunks<'a>,
input_chunk: Chunk<'a>,
output_position: WrapPoint,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
snapshot: &'a WrapSnapshot,
}
@@ -66,7 +66,7 @@ pub struct WrapBufferRows<'a> {
output_row: u32,
soft_wrapped: bool,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
impl<'a> WrapBufferRows<'a> {
@@ -86,7 +86,7 @@ impl<'a> WrapBufferRows<'a> {
impl WrapMap {
pub fn new(
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
font: Font,
font_size: Pixels,
wrap_width: Option<Pixels>,
@@ -99,7 +99,7 @@ impl WrapMap {
pending_edits: Default::default(),
interpolated_edits: Default::default(),
edits_since_sync: Default::default(),
snapshot: WrapSnapshot::new(char_snapshot),
snapshot: WrapSnapshot::new(tab_snapshot),
background_task: None,
};
this.set_wrap_width(wrap_width, cx);
@@ -117,17 +117,17 @@ impl WrapMap {
pub fn sync(
&mut self,
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut ModelContext<Self>,
) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((char_snapshot, edits));
self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx);
} else {
self.edits_since_sync = self
.edits_since_sync
.compose(self.snapshot.interpolate(char_snapshot, &edits));
.compose(self.snapshot.interpolate(tab_snapshot, &edits));
self.snapshot.interpolated = false;
}
@@ -177,11 +177,11 @@ impl WrapMap {
let (font, font_size) = self.font_with_size.clone();
let task = cx.background_executor().spawn(async move {
let mut line_wrapper = text_system.line_wrapper(font, font_size);
let char_snapshot = new_snapshot.char_snapshot.clone();
let range = CharPoint::zero()..char_snapshot.max_point();
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
let edits = new_snapshot
.update(
char_snapshot,
tab_snapshot,
&[TabEdit {
old: range.clone(),
new: range.clone(),
@@ -221,7 +221,7 @@ impl WrapMap {
} else {
let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
self.snapshot.transforms = SumTree::default();
let summary = self.snapshot.char_snapshot.text_summary();
let summary = self.snapshot.tab_snapshot.text_summary();
if !summary.lines.is_zero() {
self.snapshot
.transforms
@@ -239,8 +239,8 @@ impl WrapMap {
fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
if !self.snapshot.interpolated {
let mut to_remove_len = 0;
for (char_snapshot, _) in &self.pending_edits {
if char_snapshot.version <= self.snapshot.char_snapshot.version {
for (tab_snapshot, _) in &self.pending_edits {
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
to_remove_len += 1;
} else {
break;
@@ -262,9 +262,9 @@ impl WrapMap {
let update_task = cx.background_executor().spawn(async move {
let mut edits = Patch::default();
let mut line_wrapper = text_system.line_wrapper(font, font_size);
for (char_snapshot, tab_edits) in pending_edits {
for (tab_snapshot, tab_edits) in pending_edits {
let wrap_edits = snapshot
.update(char_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
.update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
.await;
edits = edits.compose(&wrap_edits);
}
@@ -301,11 +301,11 @@ impl WrapMap {
let was_interpolated = self.snapshot.interpolated;
let mut to_remove_len = 0;
for (char_snapshot, edits) in &self.pending_edits {
if char_snapshot.version <= self.snapshot.char_snapshot.version {
for (tab_snapshot, edits) in &self.pending_edits {
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
to_remove_len += 1;
} else {
let interpolated_edits = self.snapshot.interpolate(char_snapshot.clone(), edits);
let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
}
@@ -318,49 +318,45 @@ impl WrapMap {
}
impl WrapSnapshot {
fn new(char_snapshot: CharSnapshot) -> Self {
fn new(tab_snapshot: TabSnapshot) -> Self {
let mut transforms = SumTree::default();
let extent = char_snapshot.text_summary();
let extent = tab_snapshot.text_summary();
if !extent.lines.is_zero() {
transforms.push(Transform::isomorphic(extent), &());
}
Self {
transforms,
char_snapshot,
tab_snapshot,
interpolated: true,
}
}
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.char_snapshot.buffer_snapshot()
self.tab_snapshot.buffer_snapshot()
}
fn interpolate(
&mut self,
new_char_snapshot: CharSnapshot,
tab_edits: &[TabEdit],
) -> Patch<u32> {
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
let mut new_transforms;
if tab_edits.is_empty() {
new_transforms = self.transforms.clone();
} else {
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
let mut tab_edits_iter = tab_edits.iter().peekable();
new_transforms =
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
while let Some(edit) = tab_edits_iter.next() {
if edit.new.start > CharPoint::from(new_transforms.summary().input.lines) {
let summary = new_char_snapshot.text_summary_for_range(
CharPoint::from(new_transforms.summary().input.lines)..edit.new.start,
if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
let summary = new_tab_snapshot.text_summary_for_range(
TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
if !edit.new.is_empty() {
new_transforms.push_or_extend(Transform::isomorphic(
new_char_snapshot.text_summary_for_range(edit.new.clone()),
new_tab_snapshot.text_summary_for_range(edit.new.clone()),
));
}
@@ -369,7 +365,7 @@ impl WrapSnapshot {
if next_edit.old.start > old_cursor.end(&()) {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.char_snapshot
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -383,7 +379,7 @@ impl WrapSnapshot {
} else {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.char_snapshot
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -396,7 +392,7 @@ impl WrapSnapshot {
let old_snapshot = mem::replace(
self,
WrapSnapshot {
char_snapshot: new_char_snapshot,
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: true,
},
@@ -407,7 +403,7 @@ impl WrapSnapshot {
async fn update(
&mut self,
new_char_snapshot: CharSnapshot,
new_tab_snapshot: TabSnapshot,
tab_edits: &[TabEdit],
wrap_width: Pixels,
line_wrapper: &mut LineWrapper,
@@ -444,27 +440,27 @@ impl WrapSnapshot {
new_transforms = self.transforms.clone();
} else {
let mut row_edits = row_edits.into_iter().peekable();
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
new_transforms = old_cursor.slice(
&CharPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
&TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
Bias::Right,
&(),
);
while let Some(edit) = row_edits.next() {
if edit.new_rows.start > new_transforms.summary().input.lines.row {
let summary = new_char_snapshot.text_summary_for_range(
CharPoint(new_transforms.summary().input.lines)
..CharPoint::new(edit.new_rows.start, 0),
let summary = new_tab_snapshot.text_summary_for_range(
TabPoint(new_transforms.summary().input.lines)
..TabPoint::new(edit.new_rows.start, 0),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
let mut line = String::new();
let mut remaining = None;
let mut chunks = new_char_snapshot.chunks(
CharPoint::new(edit.new_rows.start, 0)..new_char_snapshot.max_point(),
let mut chunks = new_tab_snapshot.chunks(
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
false,
Highlights::default(),
);
@@ -511,19 +507,19 @@ impl WrapSnapshot {
}
new_transforms.extend(edit_transforms, &());
old_cursor.seek_forward(&CharPoint::new(edit.old_rows.end, 0), Bias::Right, &());
old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
if let Some(next_edit) = row_edits.peek() {
if next_edit.old_rows.start > old_cursor.end(&()).row() {
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
let summary = self.char_snapshot.text_summary_for_range(
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.append(
old_cursor.slice(
&CharPoint::new(next_edit.old_rows.start, 0),
&TabPoint::new(next_edit.old_rows.start, 0),
Bias::Right,
&(),
),
@@ -531,9 +527,9 @@ impl WrapSnapshot {
);
}
} else {
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
let summary = self.char_snapshot.text_summary_for_range(
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -546,7 +542,7 @@ impl WrapSnapshot {
let old_snapshot = mem::replace(
self,
WrapSnapshot {
char_snapshot: new_char_snapshot,
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: false,
},
@@ -599,17 +595,17 @@ impl WrapSnapshot {
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&output_start, Bias::Right, &());
let mut input_start = CharPoint(transforms.start().1 .0);
let mut input_start = TabPoint(transforms.start().1 .0);
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - transforms.start().0 .0;
}
let input_end = self
.to_char_point(output_end)
.min(self.char_snapshot.max_point());
.to_tab_point(output_end)
.min(self.tab_snapshot.max_point());
WrapChunks {
input_chunks: self.char_snapshot.chunks(
input_chunks: self.tab_snapshot.chunks(
input_start..input_end,
language_aware,
highlights,
@@ -627,7 +623,7 @@ impl WrapSnapshot {
}
pub fn line_len(&self, row: u32) -> u32 {
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
if cursor
.item()
@@ -635,7 +631,7 @@ impl WrapSnapshot {
{
let overshoot = row - cursor.start().0.row();
let tab_row = cursor.start().1.row() + overshoot;
let tab_line_len = self.char_snapshot.line_len(tab_row);
let tab_line_len = self.tab_snapshot.line_len(tab_row);
if overshoot == 0 {
cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
} else {
@@ -652,17 +648,15 @@ impl WrapSnapshot {
let start = WrapPoint::new(rows.start, 0);
let end = WrapPoint::new(rows.end, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&start, Bias::Right, &());
if let Some(transform) = cursor.item() {
let start_in_transform = start.0 - cursor.start().0 .0;
let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = CharPoint(cursor.start().1 .0 + start_in_transform);
let char_end = CharPoint(cursor.start().1 .0 + end_in_transform);
summary += &self
.char_snapshot
.text_summary_for_range(char_start..char_end);
let tab_start = TabPoint(cursor.start().1 .0 + start_in_transform);
let tab_end = TabPoint(cursor.start().1 .0 + end_in_transform);
summary += &self.tab_snapshot.text_summary_for_range(tab_start..tab_end);
} else {
debug_assert_eq!(start_in_transform.row, end_in_transform.row);
let indent_len = end_in_transform.column - start_in_transform.column;
@@ -687,9 +681,9 @@ impl WrapSnapshot {
let end_in_transform = end.0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = cursor.start().1;
let char_end = CharPoint(char_start.0 + end_in_transform);
let char_end = TabPoint(char_start.0 + end_in_transform);
summary += &self
.char_snapshot
.tab_snapshot
.text_summary_for_range(char_start..char_end);
} else {
debug_assert_eq!(end_in_transform, Point::new(1, 0));
@@ -724,14 +718,14 @@ impl WrapSnapshot {
}
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_row += start_row - transforms.start().0.row();
}
let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
let mut input_buffer_rows = self.char_snapshot.buffer_rows(input_row);
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
WrapBufferRows {
transforms,
@@ -743,26 +737,26 @@ impl WrapSnapshot {
}
}
pub fn to_char_point(&self, point: WrapPoint) -> CharPoint {
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
let mut char_point = cursor.start().1 .0;
let mut tab_point = cursor.start().1 .0;
if cursor.item().map_or(false, |t| t.is_isomorphic()) {
char_point += point.0 - cursor.start().0 .0;
tab_point += point.0 - cursor.start().0 .0;
}
CharPoint(char_point)
TabPoint(tab_point)
}
pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
self.char_snapshot.to_point(self.to_char_point(point), bias)
self.tab_snapshot.to_point(self.to_tab_point(point), bias)
}
pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
self.char_point_to_wrap_point(self.char_snapshot.make_char_point(point, bias))
self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
}
pub fn char_point_to_wrap_point(&self, point: CharPoint) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(CharPoint, WrapPoint)>(&());
pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&());
cursor.seek(&point, Bias::Right, &());
WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
}
@@ -777,10 +771,7 @@ impl WrapSnapshot {
}
}
self.char_point_to_wrap_point(
self.char_snapshot
.clip_point(self.to_char_point(point), bias),
)
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
}
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
@@ -790,7 +781,7 @@ impl WrapSnapshot {
*point.column_mut() = 0;
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
@@ -810,7 +801,7 @@ impl WrapSnapshot {
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
point.0 += Point::new(1, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
@@ -842,8 +833,8 @@ impl WrapSnapshot {
#[cfg(test)]
{
assert_eq!(
CharPoint::from(self.transforms.summary().input.lines),
self.char_snapshot.max_point()
TabPoint::from(self.transforms.summary().input.lines),
self.tab_snapshot.max_point()
);
{
@@ -856,18 +847,18 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
let mut input_buffer_rows = self.char_snapshot.buffer_rows(0);
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
let char_point = self.to_char_point(WrapPoint::new(display_row, 0));
if char_point.row() == prev_tab_row && display_row != 0 {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
}
prev_tab_row = char_point.row();
prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
@@ -889,14 +880,14 @@ impl<'a> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right, &());
let mut input_start = CharPoint(self.transforms.start().1 .0);
let mut input_start = TabPoint(self.transforms.start().1 .0);
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - self.transforms.start().0 .0;
}
let input_end = self
.snapshot
.to_char_point(output_end)
.min(self.snapshot.char_snapshot.max_point());
.to_tab_point(output_end)
.min(self.snapshot.tab_snapshot.max_point());
self.input_chunks.seek(input_start..input_end);
self.input_chunk = Chunk::default();
self.output_position = output_start;
@@ -951,11 +942,13 @@ impl<'a> Iterator for WrapChunks<'a> {
} else {
*self.output_position.column_mut() += char_len as u32;
}
if self.output_position >= transform_end {
self.transforms.next(&());
break;
}
}
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
self.input_chunk.text = suffix;
Some(Chunk {
@@ -1110,7 +1103,7 @@ impl sum_tree::Summary for TransformSummary {
}
}
impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
fn zero(_cx: &()) -> Self {
Default::default()
}
@@ -1120,7 +1113,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
}
}
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for CharPoint {
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
Ord::cmp(&self.0, &cursor_location.input.lines)
}
@@ -1168,7 +1161,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
mod tests {
use super::*;
use crate::{
display_map::{char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap},
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::{font, px, test::observe};
@@ -1220,9 +1213,9 @@ mod tests {
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = char_map.set_max_expansion_column(32);
log::info!("CharMap text: {:?}", tabs_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
let unwrapped_text = tabs_snapshot.text();
@@ -1268,7 +1261,7 @@ mod tests {
20..=39 => {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
let (tabs_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1281,7 +1274,7 @@ mod tests {
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tabs_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1305,8 +1298,8 @@ mod tests {
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (tabs_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("CharMap text: {:?}", tabs_snapshot.text());
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1352,7 +1345,7 @@ mod tests {
if tab_size.get() == 1
|| !wrapped_snapshot
.char_snapshot
.tab_snapshot
.fold_snapshot
.text()
.contains('\t')

View File

@@ -3218,7 +3218,7 @@ impl Editor {
return;
}
let selections = self.selections.all_adjusted(cx);
let selections = self.selections.all::<Point>(cx);
let mut bracket_inserted = false;
let mut edits = Vec::new();
let mut linked_edits = HashMap::<_, Vec<_>>::default();
@@ -3282,10 +3282,25 @@ impl Editor {
&bracket_pair.start[..prefix_len],
));
let is_closing_quote = if bracket_pair.end == bracket_pair.start
&& bracket_pair.start.len() == 1
{
let target = bracket_pair.start.chars().next().unwrap();
let current_line_count = snapshot
.reversed_chars_at(selection.start)
.take_while(|&c| c != '\n')
.filter(|&c| c == target)
.count();
current_line_count % 2 == 1
} else {
false
};
if autoclose
&& bracket_pair.close
&& following_text_allows_autoclose
&& preceding_text_matches_prefix
&& !is_closing_quote
{
let anchor = snapshot.anchor_before(selection.end);
new_selections.push((selection.map(|_| anchor), text.len()));
@@ -3716,7 +3731,7 @@ impl Editor {
let mut edits = Vec::new();
let mut rows = Vec::new();
for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() {
for (rows_inserted, selection) in self.selections.all::<Point>(cx).into_iter().enumerate() {
let cursor = selection.head();
let row = cursor.row;
@@ -3774,7 +3789,7 @@ impl Editor {
let mut rows = Vec::new();
let mut rows_inserted = 0;
for selection in self.selections.all_adjusted(cx) {
for selection in self.selections.all::<Point>(cx) {
let cursor = selection.head();
let row = cursor.row;
@@ -3845,7 +3860,7 @@ impl Editor {
let text: Arc<str> = text.into();
self.transact(cx, |this, cx| {
let old_selections = this.selections.all_adjusted(cx);
let old_selections = this.selections.all::<Point>(cx);
let selection_anchors = this.buffer.update(cx, |buffer, cx| {
let anchors = {
let snapshot = buffer.read(cx);
@@ -5803,7 +5818,7 @@ impl Editor {
return;
}
let mut selections = self.selections.all_adjusted(cx);
let mut selections = self.selections.all::<Point>(cx);
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
let rows_iter = selections.iter().map(|s| s.head().row);
@@ -10377,7 +10392,7 @@ impl Editor {
let selections = self
.selections
.all_adjusted(cx)
.all::<Point>(cx)
.into_iter()
.filter(|s| !s.is_empty())
.collect_vec();
@@ -10686,7 +10701,7 @@ impl Editor {
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
let selections = self.selections.all::<Point>(cx);
for selection in selections {
let range = selection.range().sorted();
@@ -10728,15 +10743,44 @@ impl Editor {
self.fold_ranges(fold_ranges, true, cx);
}
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
let fold_at_level = fold_at.level;
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut fold_ranges = Vec::new();
let mut stack = vec![(0, snapshot.max_buffer_row().0, 1)];
while let Some((mut start_row, end_row, current_level)) = stack.pop() {
while start_row < end_row {
match self.snapshot(cx).foldable_range(MultiBufferRow(start_row)) {
Some(foldable_range) => {
let nested_start_row = foldable_range.0.start.row + 1;
let nested_end_row = foldable_range.0.end.row;
if current_level == fold_at_level {
fold_ranges.push(foldable_range);
}
if current_level <= fold_at_level {
stack.push((nested_start_row, nested_end_row, current_level + 1));
}
start_row = nested_end_row + 1;
}
None => start_row += 1,
}
}
}
self.fold_ranges(fold_ranges, true, cx);
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..display_map.max_buffer_row().0 {
if let Some((foldable_range, fold_text)) =
display_map.foldable_range(MultiBufferRow(row))
{
fold_ranges.push((foldable_range, fold_text));
for row in 0..snapshot.max_buffer_row().0 {
if let Some(foldable_range) = self.snapshot(cx).foldable_range(MultiBufferRow(row)) {
fold_ranges.push(foldable_range);
}
}
@@ -10746,7 +10790,7 @@ impl Editor {
pub fn fold_recursive(&mut self, _: &actions::FoldRecursive, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
let selections = self.selections.all::<Point>(cx);
for selection in selections {
let range = selection.range().sorted();
@@ -12328,9 +12372,10 @@ impl Editor {
return;
};
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
let mut new_selections_by_buffer = HashMap::default();
for selection in self.selections.all::<usize>(cx) {
for selection in selections {
for (buffer, range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
@@ -12375,6 +12420,7 @@ impl Editor {
}
fn open_excerpts_common(&mut self, split: bool, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
if buffer.is_singleton() {
cx.propagate();
@@ -12387,7 +12433,7 @@ impl Editor {
};
let mut new_selections_by_buffer = HashMap::default();
for selection in self.selections.all::<usize>(cx) {
for selection in selections {
for (mut buffer_handle, mut range, _) in
buffer.range_to_buffer_ranges(selection.range(), cx)
{
@@ -12503,7 +12549,7 @@ impl Editor {
fn selection_replacement_ranges(
&self,
range: Range<OffsetUtf16>,
cx: &AppContext,
cx: &mut AppContext,
) -> Vec<Range<OffsetUtf16>> {
let selections = self.selections.all::<OffsetUtf16>(cx);
let newest_selection = selections

View File

@@ -1080,6 +1080,112 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_fold_at_level(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let view = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
&"
class Foo:
# Hello!
def a():
print(1)
def b():
print(2)
class Bar:
# World!
def a():
print(1)
def b():
print(2)
"
.unindent(),
cx,
);
build_editor(buffer.clone(), cx)
});
_ = view.update(cx, |view, cx| {
view.fold_at_level(&FoldAtLevel { level: 2 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:
# Hello!
def a():⋯
def b():⋯
class Bar:
# World!
def a():⋯
def b():⋯
"
.unindent(),
);
view.fold_at_level(&FoldAtLevel { level: 1 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:⋯
class Bar:⋯
"
.unindent(),
);
view.unfold_all(&UnfoldAll, cx);
view.fold_at_level(&FoldAtLevel { level: 0 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:
# Hello!
def a():
print(1)
def b():
print(2)
class Bar:
# World!
def a():
print(1)
def b():
print(2)
"
.unindent(),
);
assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
});
}
#[gpui::test]
fn test_move_cursor(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -68,7 +68,6 @@ use sum_tree::Bias;
use theme::{ActiveTheme, Appearance, PlayerColor};
use ui::prelude::*;
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
use unicode_segmentation::UnicodeSegmentation;
use util::RangeExt;
use util::ResultExt;
use workspace::{item::Item, Workspace};
@@ -337,6 +336,7 @@ impl EditorElement {
register_action(view, cx, Editor::open_url);
register_action(view, cx, Editor::open_file);
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at_level);
register_action(view, cx, Editor::fold_all);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::fold_recursive);
@@ -445,6 +445,7 @@ impl EditorElement {
register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_file);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::apply_all_diff_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::reload_file)
@@ -1026,21 +1027,23 @@ impl EditorElement {
}
let block_text = if let CursorShape::Block = selection.cursor_shape {
snapshot
.grapheme_at(cursor_position)
.display_chars_at(cursor_position)
.next()
.or_else(|| {
if cursor_column == 0 {
snapshot.placeholder_text().and_then(|s| {
s.graphemes(true).next().map(|s| s.to_owned())
})
snapshot
.placeholder_text()
.and_then(|s| s.chars().next())
.map(|c| (c, cursor_position))
} else {
None
}
})
.and_then(|grapheme| {
let text = if grapheme == "\n" {
.and_then(|(character, _)| {
let text = if character == '\n' {
SharedString::from(" ")
} else {
SharedString::from(grapheme)
SharedString::from(character.to_string())
};
let len = text.len();

View File

@@ -706,10 +706,11 @@ pub(crate) async fn find_file(
) -> Option<ResolvedPath> {
project
.update(cx, |project, cx| {
project.resolve_existing_file_path(&candidate_file_path, buffer, cx)
project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
})
.ok()?
.await
.filter(|s| s.is_file())
}
if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
@@ -1612,4 +1613,46 @@ mod tests {
assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
});
}
#[gpui::test]
async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
..Default::default()
},
cx,
)
.await;
// Insert a new file
let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
.await;
cx.set_state(indoc! {"
You can't open ../diˇr because it's a directory.
"});
// File does not exist
let screen_coord = cx.pixel_position(indoc! {"
You can't open ../diˇr because it's a directory.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
// No highlight
cx.update_editor(|editor, cx| {
assert!(editor
.snapshot(cx)
.text_highlight_ranges::<HoveredLinkState>()
.unwrap_or_default()
.1
.is_empty());
});
// Does not open the directory
cx.simulate_click(screen_coord, Modifiers::secondary_key());
cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
}
}

View File

@@ -1,7 +1,6 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
is_invisible,
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
Hover, RangeToAnchorExt,
@@ -12,7 +11,7 @@ use gpui::{
StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,
};
use itertools::Itertools;
use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
@@ -200,6 +199,7 @@ fn show_hover(
if editor.pending_rename.is_some() {
return None;
}
let snapshot = editor.snapshot(cx);
let (buffer, buffer_position) = editor
@@ -259,7 +259,7 @@ fn show_hover(
}
// If there's a diagnostic, assign it on the hover state and notify
let mut local_diagnostic = snapshot
let local_diagnostic = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
// Find the entry with the most specific range
@@ -281,42 +281,6 @@ fn show_hover(
})
});
if let Some(invisible) = snapshot
.buffer_snapshot
.chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let after = snapshot.buffer_snapshot.anchor_after(
anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
);
local_diagnostic = Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
..Default::default()
},
range: anchor..after,
})
} else if let Some(invisible) = snapshot
.buffer_snapshot
.reversed_chars_at(anchor)
.next()
.filter(|&c| is_invisible(c))
{
let before = snapshot.buffer_snapshot.anchor_before(
anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
);
local_diagnostic = Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
..Default::default()
},
range: before..anchor,
})
}
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
let text = match local_diagnostic.diagnostic.source {
Some(ref source) => {
@@ -324,6 +288,7 @@ fn show_hover(
}
None => local_diagnostic.diagnostic.message.clone(),
};
let mut border_color: Option<Hsla> = None;
let mut background_color: Option<Hsla> = None;
@@ -379,6 +344,7 @@ fn show_hover(
Markdown::new_text(text, markdown_style.clone(), None, cx, None)
})
.ok();
Some(DiagnosticPopover {
local_diagnostic,
primary_diagnostic,
@@ -466,6 +432,7 @@ fn show_hover(
cx.notify();
cx.refresh();
})?;
anyhow::Ok(())
}
.log_err()

View File

@@ -16,10 +16,10 @@ use util::RangeExt;
use workspace::Item;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight,
DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@@ -352,7 +352,11 @@ impl Editor {
None
}
pub(crate) fn apply_all_diff_hunks(&mut self, cx: &mut ViewContext<Self>) {
pub(crate) fn apply_all_diff_hunks(
&mut self,
_: &ApplyAllDiffHunks,
cx: &mut ViewContext<Self>,
) {
let buffers = self.buffer.read(cx).all_buffers();
for branch_buffer in buffers {
branch_buffer.update(cx, |branch_buffer, cx| {

View File

@@ -41,9 +41,9 @@ pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Edit
return None;
}
let project = this.project.clone()?;
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let selections = this.selections.all::<usize>(cx);
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();

View File

@@ -1,4 +1,4 @@
use crate::{Editor, EditorEvent, SemanticsProvider};
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
@@ -8,7 +8,7 @@ use project::Project;
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
use ui::prelude::*;
use ui::{prelude::*, ButtonLike, KeyBinding};
use workspace::{
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
@@ -232,7 +232,10 @@ impl ProposedChangesEditor {
impl Render for ProposedChangesEditor {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.clone()
div()
.size_full()
.key_context("ProposedChangesEditor")
.child(self.editor.clone())
}
}
@@ -331,17 +334,21 @@ impl ProposedChangesEditorToolbar {
}
impl Render for ProposedChangesEditorToolbar {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let editor = self.current_editor.clone();
Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
if let Some(editor) = &editor {
editor.update(cx, |editor, cx| {
editor.editor.update(cx, |editor, cx| {
editor.apply_all_diff_hunks(cx);
})
});
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
match &self.current_editor {
Some(editor) => {
let focus_handle = editor.focus_handle(cx);
let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx)
.map(|binding| binding.into_any_element());
button_like.children(keybinding).on_click({
move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx)
})
}
})
None => button_like.disabled(true),
}
}
}

View File

@@ -8,14 +8,15 @@ use std::{
use collections::HashMap;
use gpui::{AppContext, Model, Pixels};
use itertools::Itertools;
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use language::{Bias, Point, Selection, SelectionGoal, TextDimension};
use multi_buffer::AnchorRangeExt as _;
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, DisplayRow, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode,
ToOffset,
ToOffset, ToPoint,
};
#[derive(Debug, Clone)]
@@ -96,28 +97,68 @@ impl SelectionsCollection {
pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Option<Selection<D>> {
self.pending_anchor()
.as_ref()
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
let selection = self.pending_anchor()?;
if self.line_mode {
let map = self.display_map(cx);
let range = selection.range().to_point(&map.buffer_snapshot);
let expanded_range = map.expand_to_line(range);
let mut endpoints = map
.buffer_snapshot
.dimensions_from_points::<D>([expanded_range.start, expanded_range.end]);
let start = endpoints.next().unwrap();
let end = endpoints.next().unwrap();
Some(Selection {
id: selection.id,
start,
end,
reversed: selection.reversed,
goal: selection.goal,
})
} else {
Some(selection.map(|p| p.summary::<D>(&self.buffer(cx))))
}
}
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
pub fn all<'a, D>(&self, cx: &mut AppContext) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D>,
{
// todo!()
// let mut selections = self.all::<Point>(cx);
// if self.line_mode {
// let map = self.display_map(cx);
// for selection in &mut selections {
// let new_range = map.expand_to_line(selection.range());
// selection.start = new_range.start;
// selection.end = new_range.end;
// }
// }
let map = self.display_map(cx);
let disjoint_anchors = &self.disjoint;
let mut disjoint =
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
let mut disjoint = resolve_multiple::<Point, _>(disjoint_anchors.iter(), &self.buffer(cx))
.map(|mut selection| {
if self.line_mode {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
}
// todo!(expand selection to encompass folds and blocks)
// let start = map.display_point_to_point(map.point_to_display_point(selection.start, Bias::Left), Bias::Left);
// let end = map.display_point_to_point(map.point_to_display_point(selection.end, Bias::Right), Bias::Right);
selection
})
.peekable();
let mut pending_opt = self.pending::<D>(cx);
let mut pending_opt = self.pending::<Point>(cx);
iter::from_fn(move || {
// todo!(return merged selections)
if let Some(pending) = pending_opt.as_mut() {
while let Some(next_selection) = disjoint.peek() {
if pending.start <= next_selection.end && pending.end >= next_selection.start {
@@ -140,23 +181,10 @@ impl SelectionsCollection {
disjoint.next()
}
})
// todo!("convert selections to D in a batched way")
.collect()
}
/// Returns all of the selections, adjusted to take into account the selection line_mode
pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
let mut selections = self.all::<Point>(cx);
if self.line_mode {
let map = self.display_map(cx);
for selection in &mut selections {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
}
}
selections
}
/// Returns the newest selection, adjusted to take into account the selection line_mode
pub fn newest_adjusted(&self, cx: &mut AppContext) -> Selection<Point> {
let mut selection = self.newest::<Point>(cx);
@@ -276,14 +304,14 @@ impl SelectionsCollection {
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
self.all(cx).first().unwrap().clone()
}
pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
self.all(cx).last().unwrap().clone()
}
@@ -298,7 +326,7 @@ impl SelectionsCollection {
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Vec<Range<D>> {
self.all::<D>(cx)
.iter()
@@ -475,7 +503,7 @@ impl<'a> MutableSelectionsCollection<'a> {
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
{
let mut selections = self.all(self.cx);
let mut selections = self.collection.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
@@ -649,6 +677,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let display_map = self.display_map();
let selections = self
.collection
.all::<Point>(self.cx)
.into_iter()
.map(|selection| {
@@ -676,6 +705,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.collection
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {

View File

@@ -1,6 +1,7 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range},
path::Path,
sync::Arc,
};
@@ -66,10 +67,12 @@ impl EditorLspTestContext {
);
language_registry.add(Arc::new(language));
let root = Self::root_path();
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.insert_tree(root, json!({ "dir": { file_name.clone(): "" }}))
.await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -79,7 +82,7 @@ impl EditorLspTestContext {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
project
.update(&mut cx, |project, cx| {
project.find_or_create_worktree("/root", true, cx)
project.find_or_create_worktree(root, true, cx)
})
.await
.unwrap();
@@ -108,7 +111,7 @@ impl EditorLspTestContext {
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
}
}
@@ -123,6 +126,7 @@ impl EditorLspTestContext {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
@@ -309,6 +313,16 @@ impl EditorLspTestContext {
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\root")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/root")
}
}
impl Deref for EditorLspTestContext {

View File

@@ -17,6 +17,7 @@ use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
@@ -42,17 +43,18 @@ impl EditorTestContext {
pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
let fs = FakeFs::new(cx.executor());
// fs.insert_file("/file", "".to_owned()).await;
let root = Self::root_path();
fs.insert_tree(
"/root",
root,
serde_json::json!({
"file": "",
}),
)
.await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let project = Project::test(fs, [root], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/root/file", cx)
project.open_local_buffer(root.join("file"), cx)
})
.await
.unwrap();
@@ -71,6 +73,16 @@ impl EditorTestContext {
}
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\root")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/root")
}
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
let editor_view = editor.root_view(cx).unwrap();
Self {

View File

@@ -8,7 +8,8 @@ use collections::HashMap;
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
CodeLabel, HighlightId, Language, LanguageServerName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
use serde::Serialize;
@@ -194,6 +195,7 @@ impl LspAdapter for ExtensionLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
let delegate = delegate.clone();

View File

@@ -37,7 +37,7 @@ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,
LoadedLanguage, QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
@@ -1102,14 +1102,21 @@ impl ExtensionStore {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
let queries = load_plugin_queries(&language_path);
let tasks = std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions = serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
});
let context_provider =
std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions =
serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
});
Ok((config, queries, tasks))
Ok(LoadedLanguage {
config,
queries,
context_provider,
toolchain_provider: None,
})
},
);
}

View File

@@ -790,9 +790,9 @@ impl FileFinderDelegate {
let mut path_matches = Vec::new();
let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| {
this.abs_file_path_exists(query.path_query(), cx)
this.resolve_abs_file_path(query.path_query(), cx)
}) {
task.await
task.await.is_some()
} else {
false
};

View File

@@ -242,7 +242,7 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(2));
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
let all_selections = editor.selections.all::<Point>(cx);
assert_eq!(
all_selections.len(),
1,
@@ -317,7 +317,7 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(2));
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
let all_selections = editor.selections.all::<Point>(cx);
assert_eq!(
all_selections.len(),
1,

View File

@@ -813,6 +813,7 @@ struct FakeFsState {
root: Arc<Mutex<FakeFsEntry>>,
next_inode: u64,
next_mtime: SystemTime,
git_event_tx: smol::channel::Sender<PathBuf>,
event_txs: Vec<smol::channel::Sender<Vec<PathEvent>>>,
events_paused: bool,
buffered_events: Vec<PathEvent>,
@@ -865,14 +866,22 @@ impl FakeFsState {
let mut entry_stack = Vec::new();
'outer: loop {
let mut path_components = path.components().peekable();
let mut prefix = None;
while let Some(component) = path_components.next() {
match component {
Component::Prefix(_) => panic!("prefix paths aren't supported"),
Component::Prefix(prefix_component) => prefix = Some(prefix_component),
Component::RootDir => {
entry_stack.clear();
entry_stack.push(self.root.clone());
canonical_path.clear();
canonical_path.push("/");
match prefix {
Some(prefix_component) => {
canonical_path = PathBuf::from(prefix_component.as_os_str());
// Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
canonical_path.push(std::path::MAIN_SEPARATOR_STR);
}
None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
}
}
Component::CurDir => {}
Component::ParentDir => {
@@ -894,7 +903,7 @@ impl FakeFsState {
}
}
entry_stack.push(entry.clone());
canonical_path.push(name);
canonical_path = canonical_path.join(name);
} else {
return None;
}
@@ -956,9 +965,15 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {
/// We need to use something large enough for Windows and Unix to consider this a new file.
/// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
const SYSTEMTIME_INTERVAL: u64 = 100;
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
Arc::new(Self {
executor,
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
let this = Arc::new(Self {
executor: executor.clone(),
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
@@ -967,6 +982,7 @@ impl FakeFs {
entries: Default::default(),
git_repo_state: None,
})),
git_event_tx: tx,
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
@@ -975,7 +991,22 @@ impl FakeFs {
read_dir_call_count: 0,
metadata_call_count: 0,
}),
})
});
executor.spawn({
let this = this.clone();
async move {
while let Some(git_event) = rx.next().await {
if let Some(mut state) = this.state.try_lock() {
state.emit_event([(git_event, None)]);
} else {
panic!("Failed to lock file system state, this execution would have caused a test hang");
}
}
}
}).detach();
this
}
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
@@ -989,7 +1020,7 @@ impl FakeFs {
let new_mtime = state.next_mtime;
let new_inode = state.next_inode;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state
.write_path(path, move |entry| {
match entry {
@@ -1042,7 +1073,7 @@ impl FakeFs {
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
@@ -1169,7 +1200,12 @@ impl FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let repo_state = git_repo_state.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
});
let mut repo_state = repo_state.lock();
f(&mut repo_state);
@@ -1184,7 +1220,22 @@ impl FakeFs {
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, true, |state| {
state.branch_name = branch.map(Into::into)
let branch = branch.map(Into::into);
state.branches.extend(branch.clone());
state.current_branch_name = branch.map(Into::into)
})
}
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
self.with_git_state(dot_git, true, |state| {
if let Some(first) = branches.first() {
if state.current_branch_name.is_none() {
state.current_branch_name = Some(first.to_string())
}
}
state
.branches
.extend(branches.iter().map(ToString::to_string));
})
}
@@ -1384,15 +1435,16 @@ impl Fs for FakeFs {
let mut created_dirs = Vec::new();
let mut cur_path = PathBuf::new();
for component in path.components() {
let mut state = self.state.lock();
let should_skip = matches!(component, Component::Prefix(..) | Component::RootDir);
cur_path.push(component);
if cur_path == Path::new("/") {
if should_skip {
continue;
}
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
state.write_path(&cur_path, |entry| {
entry.or_insert_with(|| {
@@ -1418,7 +1470,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
@@ -1553,7 +1605,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let mtime = state.next_mtime;
let inode = util::post_inc(&mut state.next_inode);
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let source_entry = state.read_path(&source)?;
let content = source_entry.lock().file_content(&source)?.clone();
let mut kind = Some(PathEventKind::Created);
@@ -1823,7 +1875,12 @@ impl Fs for FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let state = git_repo_state
.get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
abs_dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
})
.clone();
Some(git::repository::FakeGitRepository::open(state))
} else {

View File

@@ -1,8 +1,9 @@
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{Context, Result};
use collections::HashMap;
use collections::{HashMap, HashSet};
use git2::BranchType;
use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
use serde::{Deserialize, Serialize};
@@ -17,7 +18,7 @@ use util::ResultExt;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub is_head: bool,
pub name: Box<str>,
pub name: SharedString,
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
@@ -41,6 +42,7 @@ pub trait GitRepository: Send + Sync {
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
fn create_branch(&self, _: &str) -> Result<()>;
fn branch_exits(&self, _: &str) -> Result<bool>;
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
}
@@ -132,6 +134,18 @@ impl GitRepository for RealGitRepository {
GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
}
fn branch_exits(&self, name: &str) -> Result<bool> {
let repo = self.repository.lock();
let branch = repo.find_branch(name, BranchType::Local);
match branch {
Ok(_) => Ok(true),
Err(e) => match e.code() {
git2::ErrorCode::NotFound => Ok(false),
_ => Err(anyhow::anyhow!(e)),
},
}
}
fn branches(&self) -> Result<Vec<Branch>> {
let repo = self.repository.lock();
let local_branches = repo.branches(Some(BranchType::Local))?;
@@ -139,7 +153,11 @@ impl GitRepository for RealGitRepository {
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
let is_head = branch.is_head();
let name = branch.name().ok().flatten().map(Box::from)?;
let name = branch
.name()
.ok()
.flatten()
.map(|name| name.to_string().into())?;
let timestamp = branch.get().peel_to_commit().ok()?.time();
let unix_timestamp = timestamp.seconds();
let timezone_offset = timestamp.offset_minutes();
@@ -201,17 +219,20 @@ impl GitRepository for RealGitRepository {
}
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct FakeGitRepository {
state: Arc<Mutex<FakeGitRepositoryState>>,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
pub path: PathBuf,
pub event_emitter: smol::channel::Sender<PathBuf>,
pub index_contents: HashMap<PathBuf, String>,
pub blames: HashMap<PathBuf, Blame>,
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
pub branch_name: Option<String>,
pub current_branch_name: Option<String>,
pub branches: HashSet<String>,
}
impl FakeGitRepository {
@@ -220,6 +241,20 @@ impl FakeGitRepository {
}
}
impl FakeGitRepositoryState {
pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
FakeGitRepositoryState {
path,
event_emitter,
index_contents: Default::default(),
blames: Default::default(),
worktree_statuses: Default::default(),
current_branch_name: Default::default(),
branches: Default::default(),
}
}
}
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
@@ -234,7 +269,7 @@ impl GitRepository for FakeGitRepository {
fn branch_name(&self) -> Option<String> {
let state = self.state.lock();
state.branch_name.clone()
state.current_branch_name.clone()
}
fn head_sha(&self) -> Option<String> {
@@ -264,18 +299,41 @@ impl GitRepository for FakeGitRepository {
}
fn branches(&self) -> Result<Vec<Branch>> {
Ok(vec![])
let state = self.state.lock();
let current_branch = &state.current_branch_name;
Ok(state
.branches
.iter()
.map(|branch_name| Branch {
is_head: Some(branch_name) == current_branch.as_ref(),
name: branch_name.into(),
unix_timestamp: None,
})
.collect())
}
fn branch_exits(&self, name: &str) -> Result<bool> {
let state = self.state.lock();
Ok(state.branches.contains(name))
}
fn change_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
state.branch_name = Some(name.to_owned());
state.current_branch_name = Some(name.to_owned());
state
.event_emitter
.try_send(state.path.clone())
.expect("Dropped repo change event");
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
state.branch_name = Some(name.to_owned());
state.branches.insert(name.to_owned());
state
.event_emitter
.try_send(state.path.clone())
.expect("Dropped repo change event");
Ok(())
}

View File

@@ -37,34 +37,34 @@ impl CursorPosition {
}
fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
self.selected_count = Default::default();
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
self.selected_count.lines += 1;
self.selected_count = Default::default();
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
{
last_selection = Some(selection);
}
}
}
self.position = last_selection.map(|s| s.head().to_point(&buffer));
for selection in editor.selections.all::<Point>(cx) {
if selection.end != selection.start {
self.selected_count.lines += (selection.end.row - selection.start.row) as usize;
if selection.end.column != 0 {
self.selected_count.lines += 1;
}
}
}
self.position = last_selection.map(|s| s.head().to_point(&buffer));
});
cx.notify();
}

View File

@@ -56,8 +56,8 @@ impl GoToLine {
}
pub fn new(active_editor: View<Editor>, cx: &mut ViewContext<Self>) -> Self {
let editor = active_editor.read(cx);
let cursor = editor.selections.last::<Point>(cx).head();
let cursor =
active_editor.update(cx, |editor, cx| editor.selections.last::<Point>(cx).head());
let line = cursor.row + 1;
let column = cursor.column + 1;

View File

@@ -256,6 +256,9 @@ pub struct AppContext {
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
pub(crate) propagate_event: bool,
pub(crate) prompt_builder: Option<PromptBuilder>,
#[cfg(any(test, feature = "test-support", debug_assertions))]
pub(crate) name: Option<&'static str>,
}
impl AppContext {
@@ -309,6 +312,9 @@ impl AppContext {
layout_id_buffer: Default::default(),
propagate_event: true,
prompt_builder: Some(PromptBuilder::Default),
#[cfg(any(test, feature = "test-support", debug_assertions))]
name: None,
}),
});
@@ -988,6 +994,7 @@ impl AppContext {
}
/// Move the global of the given type to the stack.
#[track_caller]
pub(crate) fn lease_global<G: Global>(&mut self) -> GlobalLease<G> {
GlobalLease::new(
self.globals_by_type
@@ -1319,6 +1326,12 @@ impl AppContext {
(task, is_first)
}
/// Get the name for this App.
#[cfg(any(test, feature = "test-support", debug_assertions))]
pub fn get_name(&self) -> &'static str {
self.name.as_ref().unwrap()
}
}
impl Context for AppContext {

View File

@@ -536,6 +536,15 @@ impl AnyWeakModel {
}
}
impl std::fmt::Debug for AnyWeakModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(type_name::<Self>())
.field("entity_id", &self.entity_id)
.field("entity_type", &self.entity_type)
.finish()
}
}
impl<T> From<WeakModel<T>> for AnyWeakModel {
fn from(model: WeakModel<T>) -> Self {
model.any_model

View File

@@ -478,6 +478,12 @@ impl TestAppContext {
.await
.unwrap();
}
/// Set a name for this App.
#[cfg(any(test, feature = "test-support"))]
pub fn set_name(&mut self, name: &'static str) {
self.update(|cx| cx.name = Some(name))
}
}
impl<T: 'static> Model<T> {

View File

@@ -340,6 +340,7 @@ impl Element for UniformList {
visible_range.clone(),
bounds,
item_height,
self.item_count,
cx,
);
let available_space = size(
@@ -396,6 +397,7 @@ pub trait UniformListDecoration {
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
item_height: Pixels,
item_count: usize,
cx: &mut WindowContext,
) -> AnyElement;
}

View File

@@ -57,6 +57,7 @@ pub trait UpdateGlobal {
}
impl<T: Global> UpdateGlobal for T {
#[track_caller]
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
where
C: BorrowAppContext,

View File

@@ -306,6 +306,7 @@ where
self.borrow_mut().set_global(global)
}
#[track_caller]
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global,

View File

@@ -1,7 +1,6 @@
use crate::{
black, fill, point, px, size, Bounds, Half, Hsla, LineLayout, Pixels, Point, Result,
SharedString, StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary,
WrappedLineLayout,
black, fill, point, px, size, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
StrikethroughStyle, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
};
use derive_more::{Deref, DerefMut};
use smallvec::SmallVec;
@@ -130,9 +129,8 @@ fn paint_line(
let text_system = cx.text_system().clone();
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
let mut max_glyph_size = size(px(0.), px(0.));
for (run_ix, run) in layout.runs.iter().enumerate() {
max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
@@ -141,9 +139,6 @@ fn paint_line(
wraps.next();
if let Some((background_origin, background_color)) = current_background.as_mut()
{
if glyph_origin.x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
}
cx.paint_quad(fill(
Bounds {
origin: *background_origin,
@@ -155,9 +150,6 @@ fn paint_line(
background_origin.y += line_height;
}
if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
if glyph_origin.x == underline_origin.x {
underline_origin.x -= max_glyph_size.width.half();
};
cx.paint_underline(
*underline_origin,
glyph_origin.x - underline_origin.x,
@@ -169,9 +161,6 @@ fn paint_line(
if let Some((strikethrough_origin, strikethrough_style)) =
current_strikethrough.as_mut()
{
if glyph_origin.x == strikethrough_origin.x {
strikethrough_origin.x -= max_glyph_size.width.half();
};
cx.paint_strikethrough(
*strikethrough_origin,
glyph_origin.x - strikethrough_origin.x,
@@ -190,18 +179,7 @@ fn paint_line(
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
if glyph.index >= run_end {
let mut style_run = decoration_runs.next();
// ignore style runs that apply to a partial glyph
while let Some(run) = style_run {
if glyph.index < run_end + (run.len as usize) {
break;
}
run_end += run.len as usize;
style_run = decoration_runs.next();
}
if let Some(style_run) = style_run {
if let Some(style_run) = decoration_runs.next() {
if let Some((_, background_color)) = &mut current_background {
if style_run.background_color.as_ref() != Some(background_color) {
finished_background = current_background.take();
@@ -262,14 +240,10 @@ fn paint_line(
}
if let Some((background_origin, background_color)) = finished_background {
let mut width = glyph_origin.x - background_origin.x;
if width == px(0.) {
width = px(5.)
};
cx.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(width, line_height),
size: size(glyph_origin.x - background_origin.x, line_height),
},
background_color,
));
@@ -325,10 +299,7 @@ fn paint_line(
last_line_end_x -= glyph.position.x;
}
if let Some((mut background_origin, background_color)) = current_background.take() {
if last_line_end_x == background_origin.x {
background_origin.x -= max_glyph_size.width.half()
};
if let Some((background_origin, background_color)) = current_background.take() {
cx.paint_quad(fill(
Bounds {
origin: background_origin,
@@ -338,10 +309,7 @@ fn paint_line(
));
}
if let Some((mut underline_start, underline_style)) = current_underline.take() {
if last_line_end_x == underline_start.x {
underline_start.x -= max_glyph_size.width.half()
};
if let Some((underline_start, underline_style)) = current_underline.take() {
cx.paint_underline(
underline_start,
last_line_end_x - underline_start.x,
@@ -349,10 +317,7 @@ fn paint_line(
);
}
if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
if last_line_end_x == strikethrough_start.x {
strikethrough_start.x -= max_glyph_size.width.half()
};
if let Some((strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
cx.paint_strikethrough(
strikethrough_start,
last_line_end_x - strikethrough_start.x,

View File

@@ -501,8 +501,6 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool,
/// Whether this chunk of text was originally a tab character.
pub is_tab: bool,
/// Whether this chunk of text is an invisible character.
pub is_invisible: bool,
/// An optional recipe for how the chunk should be presented.
pub renderer: Option<ChunkRenderer>,
}
@@ -1969,18 +1967,27 @@ impl Buffer {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta += new_text_length as isize - (range.end as isize - range.start as isize);
let range_len = range.end - range.start;
delta += new_text_length as isize - range_len as isize;
// Decide what range of the insertion to auto-indent, and whether
// the first line of the insertion should be considered a newly-inserted line
// or an edit to an existing line.
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
let mut first_line_is_new = true;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
if new_text.contains('\n')
&& old_start.column <= before_edit.indent_size_for_line(old_start.row).len
let old_line_start = before_edit.indent_size_for_line(old_start.row).len;
let old_line_end = before_edit.line_len(old_start.row);
if old_start.column > old_line_start {
first_line_is_new = false;
}
if !new_text.contains('\n')
&& (old_start.column + (range_len as u32) < old_line_end
|| old_line_end == old_line_start)
{
first_line_is_new = true;
first_line_is_new = false;
}
// When inserting text starting with a newline, avoid auto-indenting the
@@ -1990,7 +1997,7 @@ impl Buffer {
first_line_is_new = true;
}
// Avoid auto-indenting after the insertion.
let mut original_indent_column = None;
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
@@ -2002,6 +2009,8 @@ impl Buffer {
)
.len
}));
// Avoid auto-indenting the line after the edit.
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
}
@@ -4037,7 +4046,7 @@ impl<'a> BufferChunks<'a> {
let old_range = std::mem::replace(&mut self.range, range.clone());
self.chunks.set_range(self.range.clone());
if let Some(highlights) = self.highlights.as_mut() {
if old_range.start >= self.range.start && old_range.end <= self.range.end {
if old_range.start <= self.range.start && old_range.end >= self.range.end {
// Reuse existing highlights stack, as the new range is a subrange of the old one.
highlights
.stack
@@ -4213,6 +4222,7 @@ impl<'a> Iterator for BufferChunks<'a> {
if self.range.start == self.chunks.offset() + chunk.len() {
self.chunks.next().unwrap();
}
Some(Chunk {
text: slice,
syntax_highlight_id: highlight_id,

View File

@@ -1241,7 +1241,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
@@ -1256,6 +1255,74 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
"
.unindent()
);
// Insert a newline after the open brace. It is auto-indented
buffer.edit_via_marked_text(
&"
fn a() {«
»
c
.f
.g();
d
.f
.g();
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
ˇ
c
.f
.g();
d
.f
.g();
}
"
.unindent()
.replace("ˇ", "")
);
// Manually outdent the line. It stays outdented.
buffer.edit_via_marked_text(
&"
fn a() {
«»
c
.f
.g();
d
.f
.g();
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
c
.f
.g();
d
.f
.g();
}
"
.unindent()
);
buffer
});

View File

@@ -15,6 +15,7 @@ mod outline;
pub mod proto;
mod syntax_map;
mod task_context;
mod toolchain;
#[cfg(test)]
pub mod buffer_tests;
@@ -28,7 +29,7 @@ use futures::Future;
use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
pub use highlight_map::HighlightMap;
use http_client::HttpClient;
pub use language_registry::LanguageName;
pub use language_registry::{LanguageName, LoadedLanguage};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
use parking_lot::Mutex;
use regex::Regex;
@@ -61,6 +62,7 @@ use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
use theme::SyntaxTheme;
pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
use util::serde::default_true;
@@ -502,6 +504,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
Ok(serde_json::json!({}))
@@ -855,6 +858,7 @@ pub struct Language {
pub(crate) config: LanguageConfig,
pub(crate) grammar: Option<Arc<Grammar>>,
pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
pub(crate) toolchain: Option<Arc<dyn ToolchainLister>>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -983,6 +987,7 @@ impl Language {
})
}),
context_provider: None,
toolchain: None,
}
}
@@ -991,6 +996,11 @@ impl Language {
self
}
pub fn with_toolchain_lister(mut self, provider: Option<Arc<dyn ToolchainLister>>) -> Self {
self.toolchain = provider;
self
}
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
@@ -1361,6 +1371,10 @@ impl Language {
self.context_provider.clone()
}
pub fn toolchain_lister(&self) -> Option<Arc<dyn ToolchainLister>> {
self.toolchain.clone()
}
pub fn highlight_text<'a>(
self: &'a Arc<Self>,
text: &'a Rope,

View File

@@ -4,7 +4,7 @@ use crate::{
},
task_context::ContextProvider,
with_parser, CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
LanguageServerName, LspAdapter, PLAIN_TEXT,
LanguageServerName, LspAdapter, ToolchainLister, PLAIN_TEXT,
};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map, HashMap, HashSet};
@@ -75,6 +75,13 @@ impl<'a> From<&'a str> for LanguageName {
}
}
impl From<LanguageName> for String {
fn from(value: LanguageName) -> Self {
let value: &str = &value.0;
Self::from(value)
}
}
pub struct LanguageRegistry {
state: RwLock<LanguageRegistryState>,
language_server_download_dir: Option<Arc<Path>>,
@@ -123,16 +130,7 @@ pub struct AvailableLanguage {
name: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
load: Arc<
dyn Fn() -> Result<(
LanguageConfig,
LanguageQueries,
Option<Arc<dyn ContextProvider>>,
)>
+ 'static
+ Send
+ Sync,
>,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
loaded: bool,
}
@@ -200,6 +198,13 @@ struct LspBinaryStatusSender {
txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
}
pub struct LoadedLanguage {
pub config: LanguageConfig,
pub queries: LanguageQueries,
pub context_provider: Option<Arc<dyn ContextProvider>>,
pub toolchain_provider: Option<Arc<dyn ToolchainLister>>,
}
impl LanguageRegistry {
pub fn new(executor: BackgroundExecutor) -> Self {
let this = Self {
@@ -283,7 +288,14 @@ impl LanguageRegistry {
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || Ok((config.clone(), Default::default(), None)),
move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: Default::default(),
toolchain_provider: None,
context_provider: None,
})
},
)
}
@@ -424,14 +436,7 @@ impl LanguageRegistry {
name: LanguageName,
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
load: impl Fn() -> Result<(
LanguageConfig,
LanguageQueries,
Option<Arc<dyn ContextProvider>>,
)>
+ 'static
+ Send
+ Sync,
load: impl Fn() -> Result<LoadedLanguage> + 'static + Send + Sync,
) {
let load = Arc::new(load);
let state = &mut *self.state.write();
@@ -726,16 +731,18 @@ impl LanguageRegistry {
self.executor
.spawn(async move {
let language = async {
let (config, queries, provider) = (language_load)()?;
if let Some(grammar) = config.grammar.clone() {
let loaded_language = (language_load)()?;
if let Some(grammar) = loaded_language.config.grammar.clone() {
let grammar = Some(this.get_or_load_grammar(grammar).await?);
Language::new_with_id(id, config, grammar)
.with_context_provider(provider)
.with_queries(queries)
Language::new_with_id(id, loaded_language.config, grammar)
.with_context_provider(loaded_language.context_provider)
.with_toolchain_lister(loaded_language.toolchain_provider)
.with_queries(loaded_language.queries)
} else {
Ok(Language::new_with_id(id, config, None)
.with_context_provider(provider))
Ok(Language::new_with_id(id, loaded_language.config, None)
.with_context_provider(loaded_language.context_provider)
.with_toolchain_lister(loaded_language.toolchain_provider))
}
}
.await;

View File

@@ -0,0 +1,65 @@
//! Provides support for language toolchains.
//!
//! A language can have associated toolchains,
//! which is a set of tools used to interact with the projects written in said language.
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use gpui::{AsyncAppContext, SharedString};
use settings::WorktreeId;
use crate::LanguageName;
/// Represents a single toolchain.
#[derive(Clone, Debug, PartialEq)]
pub struct Toolchain {
/// User-facing label
pub name: SharedString,
pub path: SharedString,
pub language_name: LanguageName,
}
#[async_trait(?Send)]
pub trait ToolchainLister: Send + Sync {
async fn list(&self, _: PathBuf) -> ToolchainList;
}
#[async_trait(?Send)]
pub trait LanguageToolchainStore {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &mut AsyncAppContext,
) -> Option<Toolchain>;
}
type DefaultIndex = usize;
#[derive(Default, Clone)]
pub struct ToolchainList {
pub toolchains: Vec<Toolchain>,
pub default: Option<DefaultIndex>,
pub groups: Box<[(usize, SharedString)]>,
}
impl ToolchainList {
pub fn toolchains(&self) -> &[Toolchain] {
&self.toolchains
}
pub fn default_toolchain(&self) -> Option<Toolchain> {
self.default.and_then(|ix| self.toolchains.get(ix)).cloned()
}
pub fn group_for_index(&self, index: usize) -> Option<(usize, SharedString)> {
if index >= self.toolchains.len() {
return None;
}
let first_equal_or_greater = self
.groups
.partition_point(|(group_lower_bound, _)| group_lower_bound <= &index);
self.groups
.get(first_equal_or_greater.checked_sub(1)?)
.cloned()
}
}

View File

@@ -505,10 +505,14 @@ pub fn map_to_language_model_completion_events(
LanguageModelToolUse {
id: tool_use.id,
name: tool_use.name,
input: serde_json::Value::from_str(
&tool_use.input_json,
)
.map_err(|err| anyhow!(err))?,
input: if tool_use.input_json.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::from_str(
&tool_use.input_json,
)
.map_err(|err| anyhow!(err))?
},
},
))
})),

View File

@@ -54,6 +54,7 @@ pub struct OllamaLanguageModelProvider {
pub struct State {
http_client: Arc<dyn HttpClient>,
available_models: Vec<ollama::Model>,
fetch_model_task: Option<Task<Result<()>>>,
_subscription: Subscription,
}
@@ -89,6 +90,11 @@ impl State {
})
}
fn restart_fetch_models_task(&mut self, cx: &mut ModelContext<Self>) {
let task = self.fetch_models(cx);
self.fetch_model_task.replace(task);
}
fn authenticate(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
@@ -102,17 +108,29 @@ impl OllamaLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let this = Self {
http_client: http_client.clone(),
state: cx.new_model(|cx| State {
http_client,
available_models: Default::default(),
_subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
this.fetch_models(cx).detach();
cx.notify();
}),
state: cx.new_model(|cx| {
let subscription = cx.observe_global::<SettingsStore>({
let mut settings = AllLanguageModelSettings::get_global(cx).ollama.clone();
move |this: &mut State, cx| {
let new_settings = &AllLanguageModelSettings::get_global(cx).ollama;
if &settings != new_settings {
settings = new_settings.clone();
this.restart_fetch_models_task(cx);
cx.notify();
}
}
});
State {
http_client,
available_models: Default::default(),
fetch_model_task: None,
_subscription: subscription,
}
}),
};
this.state
.update(cx, |state, cx| state.fetch_models(cx).detach());
.update(cx, |state, cx| state.restart_fetch_models_task(cx));
this
}
}

View File

@@ -128,12 +128,16 @@ impl SyntaxTreeView {
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
let editor_state = self.editor.as_mut()?;
let editor = &editor_state.editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
let multibuffer = editor.buffer().read(cx);
let (buffer, range, excerpt_id) = multibuffer
.range_to_buffer_ranges(selection_range, cx)
.pop()?;
let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
let selection_range = editor.selections.last::<usize>(cx).range();
Some(
editor
.buffer()
.read(cx)
.range_to_buffer_ranges(selection_range, cx)
.pop()?,
)
})?;
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
// from that buffer.

View File

@@ -10,7 +10,7 @@ workspace = true
[features]
test-support = [
"tree-sitter"
"load-grammars"
]
load-grammars = [
"tree-sitter-bash",
@@ -47,6 +47,11 @@ log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
paths.workspace = true
pet.workspace = true
pet-core.workspace = true
pet-conda.workspace = true
pet-poetry.workspace = true
pet-reporter.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
@@ -82,3 +87,8 @@ text.workspace = true
theme = { workspace = true, features = ["test-support"] }
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
tree-sitter-typescript.workspace = true
tree-sitter-python.workspace = true
tree-sitter-go.workspace = true
tree-sitter-c.workspace = true
tree-sitter-css.workspace = true

View File

@@ -7,7 +7,9 @@ use feature_flags::FeatureFlagAppExt;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext};
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{
LanguageRegistry, LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
@@ -198,6 +200,7 @@ impl LspAdapter for JsonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {

View File

@@ -3,7 +3,7 @@ use gpui::{AppContext, UpdateGlobal};
use json::json_task_context;
pub use language::*;
use node_runtime::NodeRuntime;
use python::PythonContextProvider;
use python::{PythonContextProvider, PythonToolchainProvider};
use rust_embed::RustEmbed;
use settings::SettingsStore;
use smol::stream::StreamExt;
@@ -61,7 +61,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || Ok((config.clone(), load_queries($name), None)),
move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: None,
toolchain_provider: None,
})
},
);
};
($name:literal, $adapters:expr) => {
@@ -75,7 +82,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || Ok((config.clone(), load_queries($name), None)),
move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: None,
toolchain_provider: None,
})
},
);
};
($name:literal, $adapters:expr, $context_provider:expr) => {
@@ -90,11 +104,33 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.grammar.clone(),
config.matcher.clone(),
move || {
Ok((
config.clone(),
load_queries($name),
Some(Arc::new($context_provider)),
))
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: Some(Arc::new($context_provider)),
toolchain_provider: None,
})
},
);
};
($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => {
let config = load_config($name);
// typeck helper
let adapters: Vec<Arc<dyn LspAdapter>> = $adapters;
for adapter in adapters {
languages.register_lsp_adapter(config.name.clone(), adapter);
}
languages.register_language(
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: Some(Arc::new($context_provider)),
toolchain_provider: Some($toolchain_provider),
})
},
);
};
@@ -141,7 +177,8 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
vec![Arc::new(python::PythonLspAdapter::new(
node_runtime.clone(),
))],
PythonContextProvider
PythonContextProvider,
Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
);
language!(
"rust",

View File

@@ -3,9 +3,16 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::LanguageName;
use language::LanguageToolchainStore;
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use pet_core::python_environment::PythonEnvironmentKind;
use pet_core::Configuration;
use project::lsp_store::language_server_settings;
use serde_json::Value;
@@ -200,12 +207,35 @@ impl LspAdapter for PythonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
.unwrap_or_default()
let toolchain = toolchains
.active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)
.await;
cx.update(move |cx| {
let mut user_settings =
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
.unwrap_or_default();
// If python.pythonPath is not set in user config, do so using our toolchain picker.
if let Some(toolchain) = toolchain {
if user_settings.is_null() {
user_settings = Value::Object(serde_json::Map::default());
}
let object = user_settings.as_object_mut().unwrap();
if let Some(python) = object
.entry("python")
.or_insert(Value::Object(serde_json::Map::default()))
.as_object_mut()
{
python
.entry("pythonPath")
.or_insert(Value::String(toolchain.path.into()));
}
}
user_settings
})
}
}
@@ -320,6 +350,83 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String {
.to_string()
}
#[derive(Default)]
pub(crate) struct PythonToolchainProvider {}
static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
// Prioritize non-Conda environments.
PythonEnvironmentKind::Poetry,
PythonEnvironmentKind::Pipenv,
PythonEnvironmentKind::VirtualEnvWrapper,
PythonEnvironmentKind::Venv,
PythonEnvironmentKind::VirtualEnv,
PythonEnvironmentKind::Conda,
PythonEnvironmentKind::Pyenv,
PythonEnvironmentKind::GlobalPaths,
PythonEnvironmentKind::Homebrew,
];
fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
if let Some(kind) = kind {
ENV_PRIORITY_LIST
.iter()
.position(|blessed_env| blessed_env == &kind)
.unwrap_or(ENV_PRIORITY_LIST.len())
} else {
// Unknown toolchains are less useful than non-blessed ones.
ENV_PRIORITY_LIST.len() + 1
}
}
#[async_trait(?Send)]
impl ToolchainLister for PythonToolchainProvider {
async fn list(&self, worktree_root: PathBuf) -> ToolchainList {
let environment = pet_core::os_environment::EnvironmentApi::new();
let locators = pet::locators::create_locators(
Arc::new(pet_conda::Conda::from(&environment)),
Arc::new(pet_poetry::Poetry::from(&environment)),
&environment,
);
let mut config = Configuration::default();
config.workspace_directories = Some(vec![worktree_root]);
let reporter = pet_reporter::collect::create_reporter();
pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
let mut toolchains = reporter
.environments
.lock()
.ok()
.map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
toolchains.sort_by(|lhs, rhs| {
env_priority(lhs.kind)
.cmp(&env_priority(rhs.kind))
.then_with(|| lhs.executable.cmp(&rhs.executable))
});
let mut toolchains: Vec<_> = toolchains
.into_iter()
.filter_map(|toolchain| {
let name = if let Some(version) = &toolchain.version {
format!("Python {version} ({:?})", toolchain.kind?)
} else {
format!("{:?}", toolchain.kind?)
}
.into();
Some(Toolchain {
name,
path: toolchain.executable?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
})
})
.collect();
toolchains.dedup();
ToolchainList {
toolchains,
default: None,
groups: Default::default(),
}
}
}
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};

View File

@@ -5,6 +5,9 @@ line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "r#\"", end = "\"#", close = true, newline = true },
{ start = "r##\"", end = "\"##", close = true, newline = true },
{ start = "r###\"", end = "\"###", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] },

View File

@@ -3,7 +3,7 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use project::lsp_store::language_server_settings;
@@ -111,6 +111,7 @@ impl LspAdapter for TailwindLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let tailwind_user_settings = cx.update(|cx| {

View File

@@ -5,7 +5,7 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncAppContext;
use http_client::github::{build_asset_url, AssetKind, GitHubLspBinaryVersion};
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use project::lsp_store::language_server_settings;
@@ -230,6 +230,7 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let override_options = cx.update(|cx| {
@@ -325,6 +326,7 @@ impl LspAdapter for EsLintLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();

View File

@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use project::lsp_store::language_server_settings;
@@ -183,6 +183,7 @@ impl LspAdapter for VtslsLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let tsdk_path = Self::tsdk_path(delegate).await;

View File

@@ -3,7 +3,8 @@ use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncAppContext;
use language::{
language_settings::AllLanguageSettings, LanguageServerName, LspAdapter, LspAdapterDelegate,
language_settings::AllLanguageSettings, LanguageServerName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
@@ -92,6 +93,7 @@ impl LspAdapter for YamlLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let location = SettingsLocation {

View File

@@ -1177,6 +1177,8 @@ impl FakeLanguageServer {
let (stdout_writer, stdout_reader) = async_pipe::pipe();
let (notifications_tx, notifications_rx) = channel::unbounded();
let root = Self::root_path();
let mut server = LanguageServer::new_internal(
server_id,
stdin_writer,
@@ -1184,8 +1186,8 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
Path::new("/"),
Path::new("/"),
root,
root,
None,
cx.clone(),
|_| {},
@@ -1201,8 +1203,8 @@ impl FakeLanguageServer {
None::<async_pipe::PipeReader>,
Arc::new(Mutex::new(None)),
None,
Path::new("/"),
Path::new("/"),
root,
root,
None,
cx,
move |msg| {
@@ -1238,6 +1240,16 @@ impl FakeLanguageServer {
(server, fake)
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/")
}
}
#[cfg(any(test, feature = "test-support"))]

View File

@@ -234,6 +234,10 @@ impl<'a> MarkdownParser<'a> {
text.push('\n');
}
// We want to ignore any inline HTML tags in the text but keep
// the text between them
Event::InlineHtml(_) => {}
Event::Text(t) => {
text.push_str(t.as_ref());
@@ -626,6 +630,8 @@ impl<'a> MarkdownParser<'a> {
// Otherwise we need to insert the block after all the nested items
// that have been parsed so far
items.extend(block);
} else {
self.cursor += 1;
}
}
}
@@ -847,6 +853,16 @@ mod tests {
);
}
#[gpui::test]
async fn test_text_with_inline_html() {
let parsed = parse("This is a paragraph with an inline HTML <sometag>tag</sometag>.").await;
assert_eq!(
parsed.children,
vec![p("This is a paragraph with an inline HTML tag.", 0..63),],
);
}
#[gpui::test]
async fn test_raw_links_detection() {
let parsed = parse("Checkout this https://zed.dev link").await;
@@ -1090,6 +1106,26 @@ Some other content
);
}
#[gpui::test]
async fn test_list_item_with_inline_html() {
let parsed = parse(
"\
* This is a list item with an inline HTML <sometag>tag</sometag>.
",
)
.await;
assert_eq!(
parsed.children,
vec![list_item(
0..67,
1,
Unordered,
vec![p("This is a list item with an inline HTML tag.", 4..44),],
),],
);
}
#[gpui::test]
async fn test_nested_list_with_paragraph_inside() {
let parsed = parse(

View File

@@ -301,8 +301,8 @@ impl MarkdownPreviewView {
this.parse_markdown_from_active_editor(true, cx);
}
EditorEvent::SelectionsChanged { .. } => {
let editor = editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
let selection_range =
editor.update(cx, |editor, cx| editor.selections.last::<usize>(cx).range());
this.selected_block = this.get_block_index_under_cursor(selection_range);
this.list_state.scroll_to_reveal_item(this.selected_block);
cx.notify();

View File

@@ -2862,6 +2862,30 @@ impl MultiBufferSnapshot {
}
}
pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &AppContext) -> String {
let mut indent = self.indent_size_for_line(row).chars().collect::<String>();
if self.settings_at(0, cx).extend_comment_on_newline {
if let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) {
let delimiters = language_scope.line_comment_prefixes();
for delimiter in delimiters {
if *self
.chars_at(Point::new(row.0, indent.len() as u32))
.take(delimiter.chars().count())
.collect::<String>()
.as_str()
== **delimiter
{
indent.push_str(&delimiter);
break;
}
}
}
}
indent
}
pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> {
while row.0 > 0 {
row.0 -= 1;
@@ -2913,6 +2937,58 @@ impl MultiBufferSnapshot {
self.excerpts.summary().text.clone()
}
pub fn dimensions_from_points<'a, D>(
&'a self,
points: impl 'a + IntoIterator<Item = Point>,
) -> impl 'a + Iterator<Item = D>
where
D: TextDimension,
{
let mut cursor = self.excerpts.cursor::<TextSummary>(&());
let mut memoized_source_start: Option<Point> = None;
let mut points = points.into_iter();
std::iter::from_fn(move || {
let point = points.next()?;
// Clear the memoized source start if the point is in a different excerpt than previous.
if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) {
memoized_source_start = None;
}
// Now determine where the excerpt containing the point starts in its source buffer.
// We'll use this value to calculate overshoot next.
let source_start = if let Some(source_start) = memoized_source_start {
source_start
} else {
cursor.seek_forward(&point, Bias::Right, &());
if let Some(excerpt) = cursor.item() {
let source_start = excerpt.range.context.start.to_point(&excerpt.buffer);
memoized_source_start = Some(source_start);
source_start
} else {
return Some(D::from_text_summary(cursor.start()));
}
};
// First, assume the output dimension is at least the start of the excerpt containing the point
let mut output = D::from_text_summary(cursor.start());
// If the point lands within its excerpt, calculate and add the overshoot in dimension D.
if let Some(excerpt) = cursor.item() {
let overshoot = point - cursor.start().lines;
if !overshoot.is_zero() {
let end_in_excerpt = source_start + overshoot;
output.add_assign(
&excerpt
.buffer
.text_summary_for_range::<D, _>(source_start..end_in_excerpt),
);
}
}
Some(output)
})
}
pub fn text_summary_for_range<D, O>(&self, range: Range<O>) -> D
where
D: TextDimension,
@@ -4682,6 +4758,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point {
fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering {
Ord::cmp(self, &cursor_location.lines)
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
Ord::cmp(&Some(self), cursor_location)

View File

@@ -30,8 +30,10 @@ search.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
worktree.workspace = true
workspace.workspace = true

View File

@@ -24,12 +24,12 @@ use editor::{
use file_icons::FileIcons;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, ElementId,
EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement, IntoElement,
KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
Div, ElementId, EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement,
IntoElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
Render, SharedString, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
};
use itertools::Itertools;
use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
@@ -41,7 +41,8 @@ use search::{BufferSearchBar, ProjectSearchView};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use smol::channel;
use theme::SyntaxTheme;
use theme::{SyntaxTheme, ThemeSettings};
use ui::{IndentGuideColors, IndentGuideLayout};
use util::{debug_panic, RangeExt, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -254,14 +255,14 @@ impl SearchState {
#[derive(Debug)]
enum SelectedEntry {
Invalidated(Option<PanelEntry>),
Valid(PanelEntry),
Valid(PanelEntry, usize),
None,
}
impl SelectedEntry {
fn invalidate(&mut self) {
match std::mem::replace(self, SelectedEntry::None) {
Self::Valid(entry) => *self = Self::Invalidated(Some(entry)),
Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)),
Self::None => *self = Self::Invalidated(None),
other => *self = other,
}
@@ -653,13 +654,25 @@ impl OutlinePanel {
});
let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx);
let settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
let new_settings = *OutlinePanelSettings::get_global(cx);
if outline_panel_settings != new_settings {
outline_panel_settings = new_settings;
cx.notify();
}
});
let mut current_theme = ThemeSettings::get_global(cx).clone();
let settings_subscription =
cx.observe_global::<SettingsStore>(move |outline_panel, cx| {
let new_settings = OutlinePanelSettings::get_global(cx);
let new_theme = ThemeSettings::get_global(cx);
if &current_theme != new_theme {
outline_panel_settings = *new_settings;
current_theme = new_theme.clone();
for excerpts in outline_panel.excerpts.values_mut() {
for excerpt in excerpts.values_mut() {
excerpt.invalidate_outlines();
}
}
outline_panel.update_non_fs_items(cx);
} else if &outline_panel_settings != new_settings {
outline_panel_settings = *new_settings;
cx.notify();
}
});
let mut outline_panel = Self {
mode: ItemsDisplayMode::Outline,
@@ -2813,7 +2826,6 @@ impl OutlinePanel {
cx.spawn(|outline_panel, mut cx| async move {
let mut entries = Vec::new();
let mut match_candidates = Vec::new();
let mut added_contexts = HashSet::default();
let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
@@ -2935,7 +2947,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
new_folded_dirs,
folded_depth,
@@ -2974,7 +2985,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3000,7 +3010,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3037,7 +3046,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
PanelEntry::Fs(entry.clone()),
depth,
@@ -3051,7 +3059,6 @@ impl OutlinePanel {
outline_panel.add_search_entries(
&mut entries,
&mut match_candidates,
&mut added_contexts,
entry.clone(),
depth,
query.clone(),
@@ -3085,7 +3092,6 @@ impl OutlinePanel {
query.as_deref(),
&mut entries,
&mut match_candidates,
&mut added_contexts,
cx,
);
}
@@ -3101,7 +3107,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
PanelEntry::Fs(entry.clone()),
0,
@@ -3120,7 +3125,6 @@ impl OutlinePanel {
outline_panel.push_entry(
&mut entries,
&mut match_candidates,
&mut added_contexts,
track_matches,
PanelEntry::FoldedDirs(worktree_id, folded_dirs),
folded_depth,
@@ -3132,22 +3136,10 @@ impl OutlinePanel {
return Vec::new();
};
outline_panel
.update(&mut cx, |outline_panel, _| {
if matches!(outline_panel.mode, ItemsDisplayMode::Search(_)) {
cleanup_fs_entries_without_search_children(
&outline_panel.collapsed_entries,
&mut entries,
&mut match_candidates,
&mut added_contexts,
);
}
})
.ok();
let Some(query) = query else {
return entries;
};
let mut matched_ids = match_strings(
&match_candidates,
&query,
@@ -3183,7 +3175,6 @@ impl OutlinePanel {
&self,
entries: &mut Vec<CachedEntry>,
match_candidates: &mut Vec<StringMatchCandidate>,
added_contexts: &mut HashSet<String>,
track_matches: bool,
entry: PanelEntry,
depth: usize,
@@ -3209,47 +3200,39 @@ impl OutlinePanel {
if let Some(file_name) =
self.relative_path(fs_entry, cx).as_deref().map(file_name)
{
if added_contexts.insert(file_name.clone()) {
match_candidates.push(StringMatchCandidate {
id,
string: file_name.to_string(),
char_bag: file_name.chars().collect(),
});
}
match_candidates.push(StringMatchCandidate {
id,
string: file_name.to_string(),
char_bag: file_name.chars().collect(),
});
}
}
PanelEntry::FoldedDirs(worktree_id, entries) => {
let dir_names = self.dir_names_string(entries, *worktree_id, cx);
{
if added_contexts.insert(dir_names.clone()) {
match_candidates.push(StringMatchCandidate {
id,
string: dir_names.clone(),
char_bag: dir_names.chars().collect(),
});
}
match_candidates.push(StringMatchCandidate {
id,
string: dir_names.clone(),
char_bag: dir_names.chars().collect(),
});
}
}
PanelEntry::Outline(outline_entry) => match outline_entry {
OutlineEntry::Outline(_, _, outline) => {
if added_contexts.insert(outline.text.clone()) {
match_candidates.push(StringMatchCandidate {
id,
string: outline.text.clone(),
char_bag: outline.text.chars().collect(),
});
}
match_candidates.push(StringMatchCandidate {
id,
string: outline.text.clone(),
char_bag: outline.text.chars().collect(),
});
}
OutlineEntry::Excerpt(..) => {}
},
PanelEntry::Search(new_search_entry) => {
if added_contexts.insert(new_search_entry.render_data.context_text.clone()) {
match_candidates.push(StringMatchCandidate {
id,
char_bag: new_search_entry.render_data.context_text.chars().collect(),
string: new_search_entry.render_data.context_text.clone(),
});
}
match_candidates.push(StringMatchCandidate {
id,
char_bag: new_search_entry.render_data.context_text.chars().collect(),
string: new_search_entry.render_data.context_text.clone(),
});
}
}
}
@@ -3396,7 +3379,6 @@ impl OutlinePanel {
query: Option<&str>,
entries: &mut Vec<CachedEntry>,
match_candidates: &mut Vec<StringMatchCandidate>,
added_contexts: &mut HashSet<String>,
cx: &mut ViewContext<Self>,
) {
if let Some(excerpts) = self.excerpts.get(&buffer_id) {
@@ -3408,7 +3390,6 @@ impl OutlinePanel {
self.push_entry(
entries,
match_candidates,
added_contexts,
track_matches,
PanelEntry::Outline(OutlineEntry::Excerpt(
buffer_id,
@@ -3436,7 +3417,6 @@ impl OutlinePanel {
self.push_entry(
entries,
match_candidates,
added_contexts,
track_matches,
PanelEntry::Outline(OutlineEntry::Outline(
buffer_id,
@@ -3456,7 +3436,6 @@ impl OutlinePanel {
&mut self,
entries: &mut Vec<CachedEntry>,
match_candidates: &mut Vec<StringMatchCandidate>,
added_contexts: &mut HashSet<String>,
parent_entry: FsEntry,
parent_depth: usize,
filter_query: Option<String>,
@@ -3544,7 +3523,6 @@ impl OutlinePanel {
self.push_entry(
entries,
match_candidates,
added_contexts,
filter_query.is_some(),
PanelEntry::Search(new_search_entry),
depth,
@@ -3591,7 +3569,7 @@ impl OutlinePanel {
fn selected_entry(&self) -> Option<&PanelEntry> {
match &self.selected_entry {
SelectedEntry::Invalidated(entry) => entry.as_ref(),
SelectedEntry::Valid(entry) => Some(entry),
SelectedEntry::Valid(entry, _) => Some(entry),
SelectedEntry::None => None,
}
}
@@ -3600,137 +3578,21 @@ impl OutlinePanel {
if focus {
self.focus_handle.focus(cx);
}
self.selected_entry = SelectedEntry::Valid(entry);
let ix = self
.cached_entries
.iter()
.enumerate()
.find(|(_, cached_entry)| &cached_entry.entry == &entry)
.map(|(i, _)| i)
.unwrap_or_default();
self.selected_entry = SelectedEntry::Valid(entry, ix);
self.autoscroll(cx);
cx.notify();
}
}
fn cleanup_fs_entries_without_search_children(
collapsed_entries: &HashSet<CollapsedEntry>,
entries: &mut Vec<CachedEntry>,
string_match_candidates: &mut Vec<StringMatchCandidate>,
added_contexts: &mut HashSet<String>,
) {
let mut match_ids_to_remove = BTreeSet::new();
let mut previous_entry = None::<&PanelEntry>;
for (id, entry) in entries.iter().enumerate().rev() {
let has_search_items = match (previous_entry, &entry.entry) {
(Some(PanelEntry::Outline(_)), _) => unreachable!(),
(_, PanelEntry::Outline(_)) => false,
(_, PanelEntry::Search(_)) => true,
(None, PanelEntry::FoldedDirs(_, _) | PanelEntry::Fs(_)) => false,
(
Some(PanelEntry::Search(_)),
PanelEntry::FoldedDirs(_, _) | PanelEntry::Fs(FsEntry::Directory(..)),
) => false,
(Some(PanelEntry::FoldedDirs(..)), PanelEntry::FoldedDirs(..)) => true,
(
Some(PanelEntry::Search(_)),
PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..)),
) => true,
(
Some(PanelEntry::Fs(previous_fs)),
PanelEntry::FoldedDirs(folded_worktree, folded_dirs),
) => {
let expected_parent = folded_dirs.last().map(|dir_entry| dir_entry.path.as_ref());
match previous_fs {
FsEntry::ExternalFile(..) => false,
FsEntry::File(file_worktree, file_entry, ..) => {
file_worktree == folded_worktree
&& file_entry.path.parent() == expected_parent
}
FsEntry::Directory(directory_wortree, directory_entry) => {
directory_wortree == folded_worktree
&& directory_entry.path.parent() == expected_parent
}
}
}
(
Some(PanelEntry::FoldedDirs(folded_worktree, folded_dirs)),
PanelEntry::Fs(fs_entry),
) => match fs_entry {
FsEntry::File(..) | FsEntry::ExternalFile(..) => false,
FsEntry::Directory(directory_wortree, maybe_parent_directory) => {
directory_wortree == folded_worktree
&& Some(maybe_parent_directory.path.as_ref())
== folded_dirs
.first()
.and_then(|dir_entry| dir_entry.path.parent())
}
},
(Some(PanelEntry::Fs(previous_entry)), PanelEntry::Fs(maybe_parent_entry)) => {
match (previous_entry, maybe_parent_entry) {
(FsEntry::ExternalFile(..), _) | (_, FsEntry::ExternalFile(..)) => false,
(FsEntry::Directory(..) | FsEntry::File(..), FsEntry::File(..)) => false,
(
FsEntry::Directory(previous_worktree, previous_directory),
FsEntry::Directory(new_worktree, maybe_parent_directory),
) => {
previous_worktree == new_worktree
&& previous_directory.path.parent()
== Some(maybe_parent_directory.path.as_ref())
}
(
FsEntry::File(previous_worktree, previous_file, ..),
FsEntry::Directory(new_worktree, maybe_parent_directory),
) => {
previous_worktree == new_worktree
&& previous_file.path.parent()
== Some(maybe_parent_directory.path.as_ref())
}
}
}
};
if has_search_items {
previous_entry = Some(&entry.entry);
} else {
let collapsed_entries_to_check = match &entry.entry {
PanelEntry::FoldedDirs(worktree_id, entries) => entries
.iter()
.map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id))
.collect(),
PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => {
vec![CollapsedEntry::Dir(*worktree_id, entry.id)]
}
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
vec![CollapsedEntry::ExternalFile(*buffer_id)]
}
PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
vec![CollapsedEntry::File(*worktree_id, *buffer_id)]
}
PanelEntry::Search(_) | PanelEntry::Outline(_) => Vec::new(),
};
if !collapsed_entries_to_check.is_empty()
&& collapsed_entries_to_check
.iter()
.any(|collapsed_entry| collapsed_entries.contains(collapsed_entry))
{
previous_entry = Some(&entry.entry);
continue;
}
match_ids_to_remove.insert(id);
previous_entry = None;
}
}
if match_ids_to_remove.is_empty() {
return;
}
string_match_candidates.retain(|candidate| {
let retain = !match_ids_to_remove.contains(&candidate.id);
if !retain {
added_contexts.remove(&candidate.string);
}
retain
});
match_ids_to_remove.into_iter().rev().for_each(|id| {
entries.remove(id);
});
}
fn workspace_active_editor(
workspace: &Workspace,
cx: &AppContext,
@@ -3884,6 +3746,9 @@ impl Render for OutlinePanel {
let project = self.project.read(cx);
let query = self.query(cx);
let pinned = self.pinned;
let settings = OutlinePanelSettings::get_global(cx);
let indent_size = settings.indent_size;
let show_indent_guides = settings.indent_guides;
let outline_panel = v_flex()
.id("outline-panel")
@@ -4049,6 +3914,61 @@ impl Render for OutlinePanel {
})
.size_full()
.track_scroll(self.scroll_handle.clone())
.when(show_indent_guides, |list| {
list.with_decoration(
ui::indent_guides(
cx.view().clone(),
px(indent_size),
IndentGuideColors::panel(cx),
|outline_panel, range, _| {
let entries = outline_panel.cached_entries.get(range);
if let Some(entries) = entries {
entries.into_iter().map(|item| item.depth).collect()
} else {
smallvec::SmallVec::new()
}
},
)
.with_render_fn(
cx.view().clone(),
move |outline_panel, params, _| {
const LEFT_OFFSET: f32 = 14.;
let indent_size = params.indent_size;
let item_height = params.item_height;
let active_indent_guide_ix = find_active_indent_guide_ix(
outline_panel,
&params.indent_guides,
);
params
.indent_guides
.into_iter()
.enumerate()
.map(|(ix, layout)| {
let bounds = Bounds::new(
point(
px(layout.offset.x as f32) * indent_size
+ px(LEFT_OFFSET),
px(layout.offset.y as f32) * item_height,
),
size(
px(1.),
px(layout.length as f32) * item_height,
),
);
ui::RenderedIndentGuide {
bounds,
layout,
is_active: active_indent_guide_ix == Some(ix),
hitbox: None,
}
})
.collect()
},
),
)
})
})
}
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
@@ -4093,6 +4013,40 @@ impl Render for OutlinePanel {
}
}
fn find_active_indent_guide_ix(
outline_panel: &OutlinePanel,
candidates: &[IndentGuideLayout],
) -> Option<usize> {
let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
return None;
};
let target_depth = outline_panel
.cached_entries
.get(*target_ix)
.map(|cached_entry| cached_entry.depth)?;
let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
.cached_entries
.get(target_ix + 1)
.filter(|cached_entry| cached_entry.depth > target_depth)
.map(|entry| entry.depth)
{
(target_ix + 1, target_depth.saturating_sub(1))
} else {
(*target_ix, target_depth.saturating_sub(1))
};
candidates
.iter()
.enumerate()
.find(|(_, guide)| {
guide.offset.y <= target_ix
&& target_ix < guide.offset.y + guide.length
&& guide.offset.x == target_depth
})
.map(|(ix, _)| ix)
}
fn subscribe_for_editor_events(
editor: &View<Editor>,
cx: &mut ViewContext<OutlinePanel>,
@@ -4362,6 +4316,117 @@ mod tests {
});
}
#[gpui::test]
async fn test_item_filtering(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
populate_with_test_ra_project(&fs, "/rust-analyzer").await;
let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
project.read_with(cx, |project, _| {
project.languages().add(Arc::new(rust_lang()))
});
let workspace = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let outline_panel = outline_panel(&workspace, cx);
outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
workspace
.update(cx, |workspace, cx| {
ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
})
.unwrap();
let search_view = workspace
.update(cx, |workspace, cx| {
workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "param_names_for_lifetime_elision_hints";
perform_project_search(&search_view, query, cx);
search_view.update(cx, |search_view, cx| {
search_view
.results_editor()
.update(cx, |results_editor, cx| {
assert_eq!(
results_editor.display_text(cx).match_indices(query).count(),
9
);
});
});
let all_matches = r#"/
crates/
ide/src/
inlay_hints/
fn_lifetime_fn.rs
search: match config.param_names_for_lifetime_elision_hints {
search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
search: Some(it) if config.param_names_for_lifetime_elision_hints => {
search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
inlay_hints.rs
search: pub param_names_for_lifetime_elision_hints: bool,
search: param_names_for_lifetime_elision_hints: self
static_index.rs
search: param_names_for_lifetime_elision_hints: false,
rust-analyzer/src/
cli/
analysis_stats.rs
search: param_names_for_lifetime_elision_hints: true,
config.rs
search: param_names_for_lifetime_elision_hints: self"#;
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None,),
all_matches,
);
});
let filter_text = "a";
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.filter_editor.update(cx, |filter_editor, cx| {
filter_editor.set_text(filter_text, cx);
});
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None),
all_matches
.lines()
.filter(|item| item.contains(filter_text))
.collect::<Vec<_>>()
.join("\n"),
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.filter_editor.update(cx, |filter_editor, cx| {
filter_editor.set_text("", cx);
});
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
outline_panel.update(cx, |outline_panel, _| {
assert_eq!(
display_entries(&outline_panel.cached_entries, None,),
all_matches,
);
});
}
#[gpui::test]
async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -19,6 +19,7 @@ pub struct OutlinePanelSettings {
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
pub indent_guides: bool,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
}
@@ -53,6 +54,10 @@ pub struct OutlinePanelSettingsContent {
///
/// Default: 20
pub indent_size: Option<f32>,
/// Whether to show indent guides in the outline panel.
///
/// Default: true
pub indent_guides: Option<bool>,
/// Whether to reveal it in the outline panel automatically,
/// when a corresponding project entry becomes active.
/// Gitignored entries are never auto revealed.

View File

@@ -7,10 +7,11 @@ use crate::{
prettier_store::{self, PrettierStore, PrettierStoreEvent},
project_settings::{LspSettings, ProjectSettings},
relativize_path, resolve_path,
toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
yarn::YarnPathStore,
CodeAction, Completion, CoreCompletion, Hover, InlayHint, Item as _, ProjectPath,
ProjectTransaction, ResolveState, Symbol,
ProjectTransaction, ResolveState, Symbol, ToolchainStore,
};
use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
@@ -36,9 +37,9 @@ use language::{
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, LanguageName,
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, LocalFile, LspAdapter,
LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
Unclipped,
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, LanguageToolchainStore,
LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset,
ToPointUtf16, Transaction, Unclipped,
};
use lsp::{
CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag,
@@ -707,12 +708,13 @@ pub struct LspStore {
nonce: u128,
buffer_store: Model<BufferStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Option<Model<ToolchainStore>>,
buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
pub languages: Arc<LanguageRegistry>,
language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
active_entry: Option<ProjectEntryId>,
_maintain_workspace_config: Task<Result<()>>,
_maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
_maintain_buffer_languages: Task<()>,
next_diagnostic_group_id: usize,
diagnostic_summaries:
@@ -871,6 +873,7 @@ impl LspStore {
buffer_store: Model<BufferStore>,
worktree_store: Model<WorktreeStore>,
prettier_store: Model<PrettierStore>,
toolchain_store: Model<ToolchainStore>,
environment: Model<ProjectEnvironment>,
languages: Arc<LanguageRegistry>,
http_client: Arc<dyn HttpClient>,
@@ -884,9 +887,15 @@ impl LspStore {
.detach();
cx.subscribe(&prettier_store, Self::on_prettier_store_event)
.detach();
cx.subscribe(&toolchain_store, Self::on_toolchain_store_event)
.detach();
cx.observe_global::<SettingsStore>(Self::on_settings_changed)
.detach();
let _maintain_workspace_config = {
let (sender, receiver) = watch::channel();
(Self::maintain_workspace_config(receiver, cx), sender)
};
Self {
mode: LspStoreMode::Local(LocalLspStore {
supplementary_language_servers: Default::default(),
@@ -909,6 +918,7 @@ impl LspStore {
downstream_client: None,
buffer_store,
worktree_store,
toolchain_store: Some(toolchain_store),
languages: languages.clone(),
language_server_ids: Default::default(),
language_server_statuses: Default::default(),
@@ -919,7 +929,7 @@ impl LspStore {
diagnostics: Default::default(),
active_entry: None,
_maintain_workspace_config: Self::maintain_workspace_config(cx),
_maintain_workspace_config,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
}
}
@@ -942,9 +952,10 @@ impl LspStore {
})
}
pub fn new_remote(
pub(super) fn new_remote(
buffer_store: Model<BufferStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Option<Model<ToolchainStore>>,
languages: Arc<LanguageRegistry>,
upstream_client: AnyProtoClient,
project_id: u64,
@@ -954,7 +965,10 @@ impl LspStore {
.detach();
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
let _maintain_workspace_config = {
let (sender, receiver) = watch::channel();
(Self::maintain_workspace_config(receiver, cx), sender)
};
Self {
mode: LspStoreMode::Remote(RemoteLspStore {
upstream_client: Some(upstream_client),
@@ -972,7 +986,8 @@ impl LspStore {
diagnostic_summaries: Default::default(),
diagnostics: Default::default(),
active_entry: None,
_maintain_workspace_config: Self::maintain_workspace_config(cx),
toolchain_store,
_maintain_workspace_config,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
}
}
@@ -1063,6 +1078,22 @@ impl LspStore {
}
}
fn on_toolchain_store_event(
&mut self,
_: Model<ToolchainStore>,
event: &ToolchainStoreEvent,
_: &mut ModelContext<Self>,
) {
match event {
ToolchainStoreEvent::ToolchainActivated { .. } => {
self.request_workspace_config_refresh()
}
}
}
fn request_workspace_config_refresh(&mut self) {
*self._maintain_workspace_config.1.borrow_mut() = ();
}
// todo!
pub fn prettier_store(&self) -> Option<Model<PrettierStore>> {
self.as_local().map(|local| local.prettier_store.clone())
@@ -3029,17 +3060,13 @@ impl LspStore {
None
}
fn maintain_workspace_config(cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
let settings_observation = cx.observe_global::<SettingsStore>(move |_, _| {
*settings_changed_tx.borrow_mut() = ();
});
cx.spawn(move |this, mut cx| async move {
while let Some(()) = settings_changed_rx.next().await {
let servers = this.update(&mut cx, |this, cx| {
pub(crate) async fn refresh_workspace_configurations(
this: &WeakModel<Self>,
mut cx: AsyncAppContext,
) {
maybe!(async move {
let servers = this
.update(&mut cx, |this, cx| {
this.language_server_ids
.iter()
.filter_map(|((worktree_id, _), server_id)| {
@@ -3061,17 +3088,52 @@ impl LspStore {
}
})
.collect::<Vec<_>>()
})?;
})
.ok()?;
for (adapter, server, delegate) in servers {
let settings = adapter.workspace_configuration(&delegate, &mut cx).await?;
let toolchain_store = this
.update(&mut cx, |this, cx| this.toolchain_store(cx))
.ok()?;
for (adapter, server, delegate) in servers {
let settings = adapter
.workspace_configuration(&delegate, toolchain_store.clone(), &mut cx)
.await
.ok()?;
server
.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams { settings },
)
.ok();
}
server
.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams { settings },
)
.ok();
}
Some(())
})
.await;
}
fn toolchain_store(&self, cx: &AppContext) -> Arc<dyn LanguageToolchainStore> {
if let Some(toolchain_store) = self.toolchain_store.as_ref() {
toolchain_store.read(cx).as_language_toolchain_store()
} else {
Arc::new(EmptyToolchainStore)
}
}
fn maintain_workspace_config(
external_refresh_requests: watch::Receiver<()>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
let settings_observation = cx.observe_global::<SettingsStore>(move |_, _| {
*settings_changed_tx.borrow_mut() = ();
});
let mut joint_future =
futures::stream::select(settings_changed_rx, external_refresh_requests);
cx.spawn(move |this, cx| async move {
while let Some(()) = joint_future.next().await {
Self::refresh_workspace_configurations(&this, cx.clone()).await;
}
drop(settings_observation);
@@ -5517,6 +5579,9 @@ impl LspStore {
let delegate = delegate.clone();
let adapter = adapter.clone();
let this = this.clone();
let toolchains = this
.update(&mut cx, |this, cx| this.toolchain_store(cx))
.ok()?;
let mut cx = cx.clone();
async move {
let language_server = pending_server.await?;
@@ -5524,7 +5589,7 @@ impl LspStore {
let workspace_config = adapter
.adapter
.clone()
.workspace_configuration(&delegate, &mut cx)
.workspace_configuration(&delegate, toolchains.clone(), &mut cx)
.await?;
let mut initialization_options = adapter
@@ -5864,17 +5929,21 @@ impl LspStore {
}
})
.detach();
language_server
.on_request::<lsp::request::WorkspaceConfiguration, _, _>({
let adapter = adapter.adapter.clone();
let delegate = delegate.clone();
let this = this.clone();
move |params, mut cx| {
let adapter = adapter.clone();
let delegate = delegate.clone();
let this = this.clone();
async move {
let workspace_config =
adapter.workspace_configuration(&delegate, &mut cx).await?;
let toolchains =
this.update(&mut cx, |this, cx| this.toolchain_store(cx))?;
let workspace_config = adapter
.workspace_configuration(&delegate, toolchains, &mut cx)
.await?;
Ok(params
.items
.into_iter()

View File

@@ -11,6 +11,7 @@ pub mod search;
mod task_inventory;
pub mod task_store;
pub mod terminals;
pub mod toolchain_store;
pub mod worktree_store;
#[cfg(test)]
@@ -24,9 +25,7 @@ mod yarn;
use anyhow::{anyhow, Context as _, Result};
use buffer_store::{BufferStore, BufferStoreEvent};
use client::{
proto, Client, Collaborator, PendingEntitySubscription, ProjectId, TypedEnvelope, UserStore,
};
use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
@@ -46,8 +45,8 @@ use itertools::Itertools;
use language::{
language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent,
CachedLspAdapter, Capability, CodeLabel, DiagnosticEntry, Documentation, File as _, Language,
LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16, Transaction,
Unclipped,
LanguageName, LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16,
Toolchain, ToolchainList, Transaction, Unclipped,
};
use lsp::{
CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId,
@@ -103,7 +102,7 @@ pub use lsp_store::{
LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
pub use toolchain_store::ToolchainStore;
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
const MAX_SEARCH_RESULT_FILES: usize = 5_000;
const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
@@ -154,13 +153,13 @@ pub struct Project {
remotely_created_models: Arc<Mutex<RemotelyCreatedModels>>,
terminals: Terminals,
node: Option<NodeRuntime>,
hosted_project_id: Option<ProjectId>,
search_history: SearchHistory,
search_included_history: SearchHistory,
search_excluded_history: SearchHistory,
snippets: Model<SnippetProvider>,
environment: Model<ProjectEnvironment>,
settings_observer: Model<SettingsObserver>,
toolchain_store: Option<Model<ToolchainStore>>,
}
#[derive(Default)]
@@ -288,6 +287,13 @@ impl ProjectPath {
path: self.path.to_string_lossy().to_string(),
}
}
pub fn root_path(worktree_id: WorktreeId) -> Self {
Self {
worktree_id,
path: Path::new("").into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -575,6 +581,7 @@ impl Project {
LspStore::init(&client);
SettingsObserver::init(&client);
TaskStore::init(Some(&client));
ToolchainStore::init(&client);
}
pub fn local(
@@ -631,12 +638,15 @@ impl Project {
});
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
.detach();
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::local(languages.clone(), worktree_store.clone(), cx)
});
let lsp_store = cx.new_model(|cx| {
LspStore::new_local(
buffer_store.clone(),
worktree_store.clone(),
prettier_store.clone(),
toolchain_store.clone(),
environment.clone(),
languages.clone(),
client.http_client(),
@@ -671,13 +681,14 @@ impl Project {
local_handles: Vec::new(),
},
node: Some(node),
hosted_project_id: None,
search_history: Self::new_search_history(),
environment,
remotely_created_models: Default::default(),
search_included_history: Self::new_search_history(),
search_excluded_history: Self::new_search_history(),
toolchain_store: Some(toolchain_store),
}
})
}
@@ -701,7 +712,7 @@ impl Project {
let ssh_proto = ssh.read(cx).proto_client();
let worktree_store =
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0));
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
@@ -734,10 +745,14 @@ impl Project {
.detach();
let environment = ProjectEnvironment::new(&worktree_store, None, cx);
let toolchain_store = Some(cx.new_model(|cx| {
ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx)
}));
let lsp_store = cx.new_model(|cx| {
LspStore::new_remote(
buffer_store.clone(),
worktree_store.clone(),
toolchain_store.clone(),
languages.clone(),
ssh_proto.clone(),
SSH_PROJECT_ID,
@@ -789,13 +804,14 @@ impl Project {
local_handles: Vec::new(),
},
node: Some(node),
hosted_project_id: None,
search_history: Self::new_search_history(),
environment,
remotely_created_models: Default::default(),
search_included_history: Self::new_search_history(),
search_excluded_history: Self::new_search_history(),
toolchain_store,
};
let ssh = ssh.read(cx);
@@ -816,6 +832,7 @@ impl Project {
LspStore::init(&ssh_proto);
SettingsObserver::init(&ssh_proto);
TaskStore::init(Some(&ssh_proto));
ToolchainStore::init(&ssh_proto);
this
})
@@ -903,6 +920,7 @@ impl Project {
let mut lsp_store = LspStore::new_remote(
buffer_store.clone(),
worktree_store.clone(),
None,
languages.clone(),
client.clone().into(),
remote_id,
@@ -986,12 +1004,12 @@ impl Project {
local_handles: Vec::new(),
},
node: None,
hosted_project_id: None,
search_history: Self::new_search_history(),
search_included_history: Self::new_search_history(),
search_excluded_history: Self::new_search_history(),
environment: ProjectEnvironment::new(&worktree_store, None, cx),
remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())),
toolchain_store: None,
};
this.set_role(role, cx);
for worktree in worktrees {
@@ -1038,47 +1056,6 @@ impl Project {
Ok(this)
}
pub async fn hosted(
remote_id: ProjectId,
user_store: Model<UserStore>,
client: Arc<Client>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: AsyncAppContext,
) -> Result<Model<Self>> {
client.authenticate_and_connect(true, &cx).await?;
let subscriptions = [
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id.0)?),
EntitySubscription::BufferStore(
client.subscribe_to_entity::<BufferStore>(remote_id.0)?,
),
EntitySubscription::WorktreeStore(
client.subscribe_to_entity::<WorktreeStore>(remote_id.0)?,
),
EntitySubscription::LspStore(client.subscribe_to_entity::<LspStore>(remote_id.0)?),
EntitySubscription::SettingsObserver(
client.subscribe_to_entity::<SettingsObserver>(remote_id.0)?,
),
];
let response = client
.request_envelope(proto::JoinHostedProject {
project_id: remote_id.0,
})
.await?;
Self::from_join_project_response(
response,
subscriptions,
client,
true,
user_store,
languages,
fs,
cx,
)
.await
}
fn new_search_history() -> SearchHistory {
SearchHistory::new(
Some(MAX_PROJECT_SEARCH_HISTORY_SIZE),
@@ -1283,10 +1260,6 @@ impl Project {
}
}
pub fn hosted_project_id(&self) -> Option<ProjectId> {
self.hosted_project_id
}
pub fn supports_terminal(&self, _cx: &AppContext) -> bool {
if self.is_local() {
return true;
@@ -2390,6 +2363,46 @@ impl Project {
.map_err(|e| anyhow!(e))
}
pub fn available_toolchains(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<ToolchainList>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref() {
toolchain_store
.read(cx)
.list_toolchains(worktree_id, language_name, cx)
} else {
Task::ready(None)
}
}
pub fn activate_toolchain(
&self,
worktree_id: WorktreeId,
toolchain: Toolchain,
cx: &mut AppContext,
) -> Task<Option<()>> {
let Some(toolchain_store) = self.toolchain_store.clone() else {
return Task::ready(None);
};
toolchain_store.update(cx, |this, cx| {
this.activate_toolchain(worktree_id, toolchain, cx)
})
}
pub fn active_toolchain(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<Toolchain>> {
let Some(toolchain_store) = self.toolchain_store.clone() else {
return Task::ready(None);
};
toolchain_store
.read(cx)
.active_toolchain(worktree_id, language_name, cx)
}
pub fn language_server_statuses<'a>(
&'a self,
cx: &'a AppContext,
@@ -3081,7 +3094,7 @@ impl Project {
}
/// Returns the resolved version of `path`, that was found in `buffer`, if it exists.
pub fn resolve_existing_file_path(
pub fn resolve_path_in_buffer(
&self,
path: &str,
buffer: &Model<Buffer>,
@@ -3089,47 +3102,56 @@ impl Project {
) -> Task<Option<ResolvedPath>> {
let path_buf = PathBuf::from(path);
if path_buf.is_absolute() || path.starts_with("~") {
self.resolve_abs_file_path(path, cx)
self.resolve_abs_path(path, cx)
} else {
self.resolve_path_in_worktrees(path_buf, buffer, cx)
}
}
pub fn abs_file_path_exists(&self, path: &str, cx: &mut ModelContext<Self>) -> Task<bool> {
let resolve_task = self.resolve_abs_file_path(path, cx);
pub fn resolve_abs_file_path(
&self,
path: &str,
cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> {
let resolve_task = self.resolve_abs_path(path, cx);
cx.background_executor().spawn(async move {
let resolved_path = resolve_task.await;
resolved_path.is_some()
resolved_path.filter(|path| path.is_file())
})
}
fn resolve_abs_file_path(
pub fn resolve_abs_path(
&self,
path: &str,
cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> {
if self.is_local() {
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let path = expanded.as_path();
let exists = fs.is_file(path).await;
let metadata = fs.metadata(path).await.ok().flatten();
exists.then(|| ResolvedPath::AbsPath(expanded))
metadata.map(|metadata| ResolvedPath::AbsPath {
path: expanded,
is_dir: metadata.is_dir,
})
})
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
let request = ssh_client
.read(cx)
.proto_client()
.request(proto::CheckFileExists {
.request(proto::GetPathMetadata {
project_id: SSH_PROJECT_ID,
path: path.to_string(),
});
cx.background_executor().spawn(async move {
let response = request.await.log_err()?;
if response.exists {
Some(ResolvedPath::AbsPath(PathBuf::from(response.path)))
Some(ResolvedPath::AbsPath {
path: PathBuf::from(response.path),
is_dir: response.is_dir,
})
} else {
None
}
@@ -3168,10 +3190,14 @@ impl Project {
resolved.strip_prefix(root_entry_path).unwrap_or(&resolved);
worktree.entry_for_path(stripped).map(|entry| {
ResolvedPath::ProjectPath(ProjectPath {
let project_path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
})
};
ResolvedPath::ProjectPath {
project_path,
is_dir: entry.is_dir(),
}
})
})
.ok()?;
@@ -3370,6 +3396,25 @@ impl Project {
worktree.get_local_repo(&root_entry)?.repo().clone().into()
}
pub fn branches(
&self,
project_path: ProjectPath,
cx: &AppContext,
) -> Task<Result<Vec<git::repository::Branch>>> {
self.worktree_store().read(cx).branches(project_path, cx)
}
pub fn update_or_create_branch(
&self,
repository: ProjectPath,
new_branch: String,
cx: &AppContext,
) -> Task<Result<()>> {
self.worktree_store()
.read(cx)
.update_or_create_branch(repository, new_branch, cx)
}
pub fn blame_buffer(
&self,
buffer: &Model<Buffer>,
@@ -3558,6 +3603,13 @@ impl Project {
anyhow::Ok(())
})??;
// We drop `this` to avoid holding a reference in this future for too
// long.
// If we keep the reference, we might not drop the `Project` early
// enough when closing a window and it will only get releases on the
// next `flush_effects()` call.
drop(this);
let answer = rx.next().await;
Ok(LanguageServerPromptResponse {
@@ -4117,24 +4169,41 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
/// or an AbsPath and that *exists*.
#[derive(Debug, Clone)]
pub enum ResolvedPath {
ProjectPath(ProjectPath),
AbsPath(PathBuf),
ProjectPath {
project_path: ProjectPath,
is_dir: bool,
},
AbsPath {
path: PathBuf,
is_dir: bool,
},
}
impl ResolvedPath {
pub fn abs_path(&self) -> Option<&Path> {
match self {
Self::AbsPath(path) => Some(path.as_path()),
Self::AbsPath { path, .. } => Some(path.as_path()),
_ => None,
}
}
pub fn project_path(&self) -> Option<&ProjectPath> {
match self {
Self::ProjectPath(path) => Some(&path),
Self::ProjectPath { project_path, .. } => Some(&project_path),
_ => None,
}
}
pub fn is_file(&self) -> bool {
!self.is_dir()
}
pub fn is_dir(&self) -> bool {
match self {
Self::ProjectPath { is_dir, .. } => *is_dir,
Self::AbsPath { is_dir, .. } => *is_dir,
}
}
}
impl Item for Buffer {

View File

@@ -0,0 +1,416 @@
use std::sync::Arc;
use anyhow::{bail, Result};
use async_trait::async_trait;
use collections::BTreeMap;
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
WeakModel,
};
use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use settings::WorktreeId;
use util::ResultExt as _;
use crate::worktree_store::WorktreeStore;
pub struct ToolchainStore(ToolchainStoreInner);
enum ToolchainStoreInner {
Local(Model<LocalToolchainStore>, #[allow(dead_code)] Subscription),
Remote(Model<RemoteToolchainStore>),
}
impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
impl ToolchainStore {
pub fn init(client: &AnyProtoClient) {
client.add_model_request_handler(Self::handle_activate_toolchain);
client.add_model_request_handler(Self::handle_list_toolchains);
client.add_model_request_handler(Self::handle_active_toolchain);
}
pub fn local(
languages: Arc<LanguageRegistry>,
worktree_store: Model<WorktreeStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let model = cx.new_model(|_| LocalToolchainStore {
languages,
worktree_store,
active_toolchains: Default::default(),
});
let subscription = cx.subscribe(&model, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
});
Self(ToolchainStoreInner::Local(model, subscription))
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut AppContext) -> Self {
Self(ToolchainStoreInner::Remote(
cx.new_model(|_| RemoteToolchainStore { client, project_id }),
))
}
pub(crate) fn activate_toolchain(
&self,
worktree_id: WorktreeId,
toolchain: Toolchain,
cx: &mut AppContext,
) -> Task<Option<()>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => local.update(cx, |this, cx| {
this.activate_toolchain(worktree_id, toolchain, cx)
}),
ToolchainStoreInner::Remote(remote) => {
remote
.read(cx)
.activate_toolchain(worktree_id, toolchain, cx)
}
}
}
pub(crate) fn list_toolchains(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<ToolchainList>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
local
.read(cx)
.list_toolchains(worktree_id, language_name, cx)
}
ToolchainStoreInner::Remote(remote) => {
remote
.read(cx)
.list_toolchains(worktree_id, language_name, cx)
}
}
}
pub(crate) fn active_toolchain(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<Toolchain>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
local
.read(cx)
.active_toolchain(worktree_id, language_name, cx)
}
ToolchainStoreInner::Remote(remote) => {
remote
.read(cx)
.active_toolchain(worktree_id, language_name, cx)
}
}
}
async fn handle_activate_toolchain(
this: Model<Self>,
envelope: TypedEnvelope<proto::ActivateToolchain>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
this.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let Some(toolchain) = envelope.payload.toolchain else {
bail!("Missing `toolchain` in payload");
};
let toolchain = Toolchain {
name: toolchain.name.into(),
path: toolchain.path.into(),
language_name,
};
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
Ok(this.activate_toolchain(worktree_id, toolchain, cx))
})??
.await;
Ok(proto::Ack {})
}
async fn handle_active_toolchain(
this: Model<Self>,
envelope: TypedEnvelope<proto::ActiveToolchain>,
mut cx: AsyncAppContext,
) -> Result<proto::ActiveToolchainResponse> {
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
this.active_toolchain(worktree_id, language_name, cx)
})?
.await;
Ok(proto::ActiveToolchainResponse {
toolchain: toolchain.map(|toolchain| proto::Toolchain {
name: toolchain.name.into(),
path: toolchain.path.into(),
}),
})
}
async fn handle_list_toolchains(
this: Model<Self>,
envelope: TypedEnvelope<proto::ListToolchains>,
mut cx: AsyncAppContext,
) -> Result<proto::ListToolchainsResponse> {
let toolchains = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
this.list_toolchains(worktree_id, language_name, cx)
})?
.await;
let has_values = toolchains.is_some();
let groups = if let Some(toolchains) = &toolchains {
toolchains
.groups
.iter()
.filter_map(|group| {
Some(proto::ToolchainGroup {
start_index: u64::try_from(group.0).ok()?,
name: String::from(group.1.as_ref()),
})
})
.collect()
} else {
vec![]
};
let toolchains = if let Some(toolchains) = toolchains {
toolchains
.toolchains
.into_iter()
.map(|toolchain| proto::Toolchain {
name: toolchain.name.to_string(),
path: toolchain.path.to_string(),
})
.collect::<Vec<_>>()
} else {
vec![]
};
Ok(proto::ListToolchainsResponse {
has_values,
toolchains,
groups,
})
}
pub(crate) fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
}
}
}
struct LocalToolchainStore {
languages: Arc<LanguageRegistry>,
worktree_store: Model<WorktreeStore>,
active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>,
}
#[async_trait(?Send)]
impl language::LanguageToolchainStore for LocalStore {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &mut AsyncAppContext,
) -> Option<Toolchain> {
self.0
.update(cx, |this, cx| {
this.active_toolchain(worktree_id, language_name, cx)
})
.ok()?
.await
}
}
#[async_trait(?Send)]
impl language::LanguageToolchainStore for RemoteStore {
async fn active_toolchain(
self: Arc<Self>,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &mut AsyncAppContext,
) -> Option<Toolchain> {
self.0
.update(cx, |this, cx| {
this.active_toolchain(worktree_id, language_name, cx)
})
.ok()?
.await
}
}
pub(crate) struct EmptyToolchainStore;
#[async_trait(?Send)]
impl language::LanguageToolchainStore for EmptyToolchainStore {
async fn active_toolchain(
self: Arc<Self>,
_: WorktreeId,
_: LanguageName,
_: &mut AsyncAppContext,
) -> Option<Toolchain> {
None
}
}
struct LocalStore(WeakModel<LocalToolchainStore>);
struct RemoteStore(WeakModel<RemoteToolchainStore>);
#[derive(Clone)]
pub(crate) enum ToolchainStoreEvent {
ToolchainActivated,
}
impl EventEmitter<ToolchainStoreEvent> for LocalToolchainStore {}
impl LocalToolchainStore {
pub(crate) fn activate_toolchain(
&self,
worktree_id: WorktreeId,
toolchain: Toolchain,
cx: &mut ModelContext<Self>,
) -> Task<Option<()>> {
cx.spawn(move |this, mut cx| async move {
this.update(&mut cx, |this, cx| {
this.active_toolchains.insert(
(worktree_id, toolchain.language_name.clone()),
toolchain.clone(),
);
cx.emit(ToolchainStoreEvent::ToolchainActivated);
})
.ok();
Some(())
})
}
pub(crate) fn list_toolchains(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<ToolchainList>> {
let registry = self.languages.clone();
let Some(root) = self
.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
else {
return Task::ready(None);
};
cx.spawn(|_| async move {
let language = registry.language_for_name(&language_name.0).await.ok()?;
let toolchains = language.toolchain_lister()?.list(root.to_path_buf()).await;
Some(toolchains)
})
}
pub(crate) fn active_toolchain(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
_: &AppContext,
) -> Task<Option<Toolchain>> {
Task::ready(
self.active_toolchains
.get(&(worktree_id, language_name))
.cloned(),
)
}
}
struct RemoteToolchainStore {
client: AnyProtoClient,
project_id: u64,
}
impl RemoteToolchainStore {
pub(crate) fn activate_toolchain(
&self,
worktree_id: WorktreeId,
toolchain: Toolchain,
cx: &AppContext,
) -> Task<Option<()>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.spawn(move |_| async move {
let _ = client
.request(proto::ActivateToolchain {
project_id,
worktree_id: worktree_id.to_proto(),
language_name: toolchain.language_name.into(),
toolchain: Some(proto::Toolchain {
name: toolchain.name.into(),
path: toolchain.path.into(),
}),
})
.await
.log_err()?;
Some(())
})
}
pub(crate) fn list_toolchains(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<ToolchainList>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.spawn(move |_| async move {
let response = client
.request(proto::ListToolchains {
project_id,
worktree_id: worktree_id.to_proto(),
language_name: language_name.clone().into(),
})
.await
.log_err()?;
if !response.has_values {
return None;
}
let toolchains = response
.toolchains
.into_iter()
.map(|toolchain| Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
path: toolchain.path.into(),
})
.collect();
let groups = response
.groups
.into_iter()
.filter_map(|group| {
Some((usize::try_from(group.start_index).ok()?, group.name.into()))
})
.collect();
Some(ToolchainList {
toolchains,
default: None,
groups,
})
})
}
pub(crate) fn active_toolchain(
&self,
worktree_id: WorktreeId,
language_name: LanguageName,
cx: &AppContext,
) -> Task<Option<Toolchain>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.spawn(move |_| async move {
let response = client
.request(proto::ActiveToolchain {
project_id,
worktree_id: worktree_id.to_proto(),
language_name: language_name.clone().into(),
})
.await
.log_err()?;
response.toolchain.map(|toolchain| Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
path: toolchain.path.into(),
})
})
}
}

View File

@@ -73,6 +73,8 @@ impl WorktreeStore {
client.add_model_request_handler(Self::handle_copy_project_entry);
client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_expand_project_entry);
client.add_model_request_handler(Self::handle_git_branches);
client.add_model_request_handler(Self::handle_update_branch);
}
pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
@@ -127,6 +129,13 @@ impl WorktreeStore {
.find(|worktree| worktree.read(cx).id() == id)
}
pub fn current_branch(&self, repository: ProjectPath, cx: &AppContext) -> Option<Arc<str>> {
self.worktree_for_id(repository.worktree_id, cx)?
.read(cx)
.git_entry(repository.path)?
.branch()
}
pub fn worktree_for_entry(
&self,
entry_id: ProjectEntryId,
@@ -836,6 +845,131 @@ impl WorktreeStore {
Ok(())
}
pub fn branches(
&self,
project_path: ProjectPath,
cx: &AppContext,
) -> Task<Result<Vec<git::repository::Branch>>> {
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
};
match worktree.read(cx) {
Worktree::Local(local_worktree) => {
let branches = util::maybe!({
let worktree_error = |error| {
format!(
"{} for worktree {}",
error,
local_worktree.abs_path().to_string_lossy()
)
};
let entry = local_worktree
.git_entry(project_path.path)
.with_context(|| worktree_error("No git entry found"))?;
let repo = local_worktree
.get_local_repo(&entry)
.with_context(|| worktree_error("No repository found"))?
.repo()
.clone();
repo.branches()
});
Task::ready(branches)
}
Worktree::Remote(remote_worktree) => {
let request = remote_worktree.client().request(proto::GitBranches {
project_id: remote_worktree.project_id(),
repository: Some(proto::ProjectPath {
worktree_id: project_path.worktree_id.to_proto(),
path: project_path.path.to_string_lossy().to_string(), // Root path
}),
});
cx.background_executor().spawn(async move {
let response = request.await?;
let branches = response
.branches
.into_iter()
.map(|proto_branch| git::repository::Branch {
is_head: proto_branch.is_head,
name: proto_branch.name.into(),
unix_timestamp: proto_branch
.unix_timestamp
.map(|timestamp| timestamp as i64),
})
.collect();
Ok(branches)
})
}
}
}
pub fn update_or_create_branch(
&self,
repository: ProjectPath,
new_branch: String,
cx: &AppContext,
) -> Task<Result<()>> {
let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
};
match worktree.read(cx) {
Worktree::Local(local_worktree) => {
let result = util::maybe!({
let worktree_error = |error| {
format!(
"{} for worktree {}",
error,
local_worktree.abs_path().to_string_lossy()
)
};
let entry = local_worktree
.git_entry(repository.path)
.with_context(|| worktree_error("No git entry found"))?;
let repo = local_worktree
.get_local_repo(&entry)
.with_context(|| worktree_error("No repository found"))?
.repo()
.clone();
if !repo.branch_exits(&new_branch)? {
repo.create_branch(&new_branch)?;
}
repo.change_branch(&new_branch)?;
Ok(())
});
Task::ready(result)
}
Worktree::Remote(remote_worktree) => {
let request = remote_worktree.client().request(proto::UpdateGitBranch {
project_id: remote_worktree.project_id(),
repository: Some(proto::ProjectPath {
worktree_id: repository.worktree_id.to_proto(),
path: repository.path.to_string_lossy().to_string(), // Root path
}),
branch_name: new_branch,
});
cx.background_executor().spawn(async move {
request.await?;
Ok(())
})
}
}
}
async fn filter_paths(
fs: &Arc<dyn Fs>,
mut input: Receiver<MatchingEntry>,
@@ -917,6 +1051,61 @@ impl WorktreeStore {
.ok_or_else(|| anyhow!("invalid request"))?;
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
}
pub async fn handle_git_branches(
this: Model<Self>,
branches: TypedEnvelope<proto::GitBranches>,
cx: AsyncAppContext,
) -> Result<proto::GitBranchesResponse> {
let project_path = branches
.payload
.repository
.clone()
.context("Invalid GitBranches call")?;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
path: Path::new(&project_path.path).into(),
};
let branches = this
.read_with(&cx, |this, cx| this.branches(project_path, cx))?
.await?;
Ok(proto::GitBranchesResponse {
branches: branches
.into_iter()
.map(|branch| proto::Branch {
is_head: branch.is_head,
name: branch.name.to_string(),
unix_timestamp: branch.unix_timestamp.map(|timestamp| timestamp as u64),
})
.collect(),
})
}
pub async fn handle_update_branch(
this: Model<Self>,
update_branch: TypedEnvelope<proto::UpdateGitBranch>,
cx: AsyncAppContext,
) -> Result<proto::Ack> {
let project_path = update_branch
.payload
.repository
.clone()
.context("Invalid GitBranches call")?;
let project_path = ProjectPath {
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
path: Path::new(&project_path.path).into(),
};
let new_branch = update_branch.payload.branch_name;
this.read_with(&cx, |this, cx| {
this.update_or_create_branch(project_path, new_branch, cx)
})?
.await?;
Ok(proto::Ack {})
}
}
#[derive(Clone, Debug)]

View File

@@ -2821,6 +2821,17 @@ impl ProjectPanel {
return None;
}
let scroll_handle = self.scroll_handle.0.borrow();
let longest_item_width = scroll_handle
.last_item_size
.filter(|size| size.contents.width > size.item.width)?
.contents
.width
.0 as f64;
if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
return None;
}
Some(
div()
.occlude()

View File

@@ -196,8 +196,6 @@ message Envelope {
GetImplementation get_implementation = 162;
GetImplementationResponse get_implementation_response = 163;
JoinHostedProject join_hosted_project = 164;
CountLanguageModelTokens count_language_model_tokens = 230;
CountLanguageModelTokensResponse count_language_model_tokens_response = 231;
GetCachedEmbeddings get_cached_embeddings = 189;
@@ -261,9 +259,6 @@ message Envelope {
CloseBuffer close_buffer = 245;
UpdateUserSettings update_user_settings = 246;
CheckFileExists check_file_exists = 255;
CheckFileExistsResponse check_file_exists_response = 256;
ShutdownRemoteServer shutdown_remote_server = 257;
RemoveWorktree remove_worktree = 258;
@@ -281,12 +276,25 @@ message Envelope {
FlushBufferedMessages flush_buffered_messages = 267;
LanguageServerPromptRequest language_server_prompt_request = 268;
LanguageServerPromptResponse language_server_prompt_response = 269; // current max
}
LanguageServerPromptResponse language_server_prompt_response = 269;
GitBranches git_branches = 270;
GitBranchesResponse git_branches_response = 271;
UpdateGitBranch update_git_branch = 272;
ListToolchains list_toolchains = 273;
ListToolchainsResponse list_toolchains_response = 274;
ActivateToolchain activate_toolchain = 275;
ActiveToolchain active_toolchain = 276;
ActiveToolchainResponse active_toolchain_response = 277;
GetPathMetadata get_path_metadata = 278;
GetPathMetadataResponse get_path_metadata_response = 279; // current max
}
reserved 87 to 88;
reserved 158 to 161;
reserved 164;
reserved 166 to 169;
reserved 177 to 185;
reserved 188;
@@ -297,6 +305,7 @@ message Envelope {
reserved 221;
reserved 224 to 229;
reserved 247 to 254;
reserved 255 to 256;
}
// Messages
@@ -518,11 +527,6 @@ message JoinProject {
uint64 project_id = 1;
}
message JoinHostedProject {
uint64 project_id = 1;
}
message ListRemoteDirectory {
uint64 dev_server_id = 1;
string path = 2;
@@ -1289,13 +1293,7 @@ message UpdateChannels {
repeated ChannelMessageId latest_channel_message_ids = 8;
repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
repeated HostedProject hosted_projects = 10;
repeated uint64 deleted_hosted_projects = 11;
reserved 12;
reserved 13;
reserved 14;
reserved 15;
reserved 10 to 15;
}
message UpdateUserChannels {
@@ -1324,13 +1322,6 @@ message ChannelParticipants {
repeated uint64 participant_user_ids = 2;
}
message HostedProject {
uint64 project_id = 1;
uint64 channel_id = 2;
string name = 3;
ChannelVisibility visibility = 4;
}
message JoinChannel {
uint64 channel_id = 1;
}
@@ -2367,14 +2358,15 @@ message UpdateUserSettings {
}
}
message CheckFileExists {
message GetPathMetadata {
uint64 project_id = 1;
string path = 2;
}
message CheckFileExistsResponse {
message GetPathMetadataResponse {
bool exists = 1;
string path = 2;
bool is_dir = 3;
}
message ShutdownRemoteServer {}
@@ -2407,7 +2399,6 @@ message GetPermalinkToLine {
message GetPermalinkToLineResponse {
string permalink = 1;
}
message FlushBufferedMessages {}
message FlushBufferedMessagesResponse {}
@@ -2432,3 +2423,64 @@ message LanguageServerPromptRequest {
message LanguageServerPromptResponse {
optional uint64 action_response = 1;
}
message ListToolchains {
uint64 project_id = 1;
uint64 worktree_id = 2;
string language_name = 3;
}
message Toolchain {
string name = 1;
string path = 2;
}
message ToolchainGroup {
uint64 start_index = 1;
string name = 2;
}
message ListToolchainsResponse {
repeated Toolchain toolchains = 1;
bool has_values = 2;
repeated ToolchainGroup groups = 3;
}
message ActivateToolchain {
uint64 project_id = 1;
uint64 worktree_id = 2;
Toolchain toolchain = 3;
string language_name = 4;
}
message ActiveToolchain {
uint64 project_id = 1;
uint64 worktree_id = 2;
string language_name = 3;
}
message ActiveToolchainResponse {
optional Toolchain toolchain = 1;
}
message Branch {
bool is_head = 1;
string name = 2;
optional uint64 unix_timestamp = 3;
}
message GitBranches {
uint64 project_id = 1;
ProjectPath repository = 2;
}
message GitBranchesResponse {
repeated Branch branches = 1;
}
message UpdateGitBranch {
uint64 project_id = 1;
string branch_name = 2;
ProjectPath repository = 3;
}

View File

@@ -228,7 +228,6 @@ messages!(
(JoinChannelChat, Foreground),
(JoinChannelChatResponse, Foreground),
(JoinProject, Foreground),
(JoinHostedProject, Foreground),
(JoinProjectResponse, Foreground),
(JoinRoom, Foreground),
(JoinRoomResponse, Foreground),
@@ -344,8 +343,6 @@ messages!(
(FindSearchCandidatesResponse, Background),
(CloseBuffer, Foreground),
(UpdateUserSettings, Foreground),
(CheckFileExists, Background),
(CheckFileExistsResponse, Background),
(ShutdownRemoteServer, Foreground),
(RemoveWorktree, Foreground),
(LanguageServerLog, Foreground),
@@ -357,6 +354,16 @@ messages!(
(FlushBufferedMessages, Foreground),
(LanguageServerPromptRequest, Foreground),
(LanguageServerPromptResponse, Foreground),
(GitBranches, Background),
(GitBranchesResponse, Background),
(UpdateGitBranch, Background),
(ListToolchains, Foreground),
(ListToolchainsResponse, Foreground),
(ActivateToolchain, Foreground),
(ActiveToolchain, Foreground),
(ActiveToolchainResponse, Foreground),
(GetPathMetadata, Background),
(GetPathMetadataResponse, Background)
);
request_messages!(
@@ -408,7 +415,6 @@ request_messages!(
(JoinChannel, JoinRoomResponse),
(JoinChannelBuffer, JoinChannelBufferResponse),
(JoinChannelChat, JoinChannelChatResponse),
(JoinHostedProject, JoinProjectResponse),
(JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse),
(LeaveChannelBuffer, Ack),
@@ -466,13 +472,18 @@ request_messages!(
(SynchronizeContexts, SynchronizeContextsResponse),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(AddWorktree, AddWorktreeResponse),
(CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack),
(RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse),
(FlushBufferedMessages, Ack),
(LanguageServerPromptRequest, LanguageServerPromptResponse),
(GitBranches, GitBranchesResponse),
(UpdateGitBranch, Ack),
(ListToolchains, ListToolchainsResponse),
(ActivateToolchain, Ack),
(ActiveToolchain, ActiveToolchainResponse),
(GetPathMetadata, GetPathMetadataResponse)
);
entity_messages!(
@@ -544,13 +555,18 @@ entity_messages!(
SynchronizeContexts,
LspExtSwitchSourceHeader,
UpdateUserSettings,
CheckFileExists,
LanguageServerLog,
Toast,
HideToast,
OpenServerSettings,
GetPermalinkToLine,
LanguageServerPromptRequest
LanguageServerPromptRequest,
GitBranches,
UpdateGitBranch,
ListToolchains,
ActivateToolchain,
ActiveToolchain,
GetPathMetadata
);
entity_messages!(

View File

@@ -11,7 +11,7 @@ use ui::{
};
use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
use crate::{open_ssh_project, SshSettings};
use crate::open_ssh_project;
enum Host {
RemoteProject,
@@ -102,16 +102,6 @@ impl DisconnectedOverlay {
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(move |_, mut cx| async move {
let nickname = cx
.update(|cx| {
SshSettings::get_global(cx).nickname_for(
&connection_options.host,
connection_options.port,
&connection_options.username,
)
})
.ok()
.flatten();
open_ssh_project(
connection_options,
paths,
@@ -120,7 +110,6 @@ impl DisconnectedOverlay {
replace_window: Some(window),
..Default::default()
},
nickname,
&mut cx,
)
.await?;

View File

@@ -1,7 +1,6 @@
pub mod disconnected_overlay;
mod remote_servers;
mod ssh_connections;
use remote::SshConnectionOptions;
pub use ssh_connections::open_ssh_project;
use disconnected_overlay::DisconnectedOverlay;
@@ -331,23 +330,12 @@ impl PickerDelegate for RecentProjectsDelegate {
..Default::default()
};
let args = SshSettings::get_global(cx).args_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let nickname = SshSettings::get_global(cx).nickname_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
password: None,
args,
};
let connection_options = SshSettings::get_global(cx)
.connection_options_for(
ssh_project.host.clone(),
ssh_project.port,
ssh_project.user.clone(),
);
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
@@ -357,7 +345,6 @@ impl PickerDelegate for RecentProjectsDelegate {
paths,
app_state,
open_options,
nickname,
&mut cx,
)
.await

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