Compare commits

...

129 Commits

Author SHA1 Message Date
Conrad Irwin
5db7e8f89e collab 0.42.0 2024-01-26 12:27:37 -07:00
Piotr Osiewicz
1761e60362 Improve performance of JSON schema creation (#6770)
JSON LSP adapter now caches the schema. `workspace_configuration` is
back to being async, and we are also no longer asking for font names
twice while constructing the schema.
Release Notes:
- Improved performance when opening the .json files.

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-01-26 19:54:45 +01:00
Mikayla Maki
0a124a9908 Feature: PHP: Add readonly keyword to keyword list (#6798)
Release Notes:

- Added `readonly` to keyword list for PHP
([#6797](https://github.com/zed-industries/zed/issues/6797)).
2024-01-26 10:46:18 -08:00
Marshall Bowers
f5f7be1500 Add experimental.theme_overrides to settings file (#6791)
This PR adds **experimental** support for overriding theme values in the
current theme.

Be advised that both the existence of this setting and the structure of
the theme itself are subject to change.

But this is a first step towards allowing Zed users to customize or
bring their own themes.

### How it works

There is a new `experimental.theme_overrides` setting in
`settings.json`.

This accepts an object containing overrides for values in the theme. All
values are optional, and will be overlaid on top of whatever theme you
currently have set by the `theme` field.

There is JSON schema support to show which values are supported.

### Example

Here's an example of it in action:


https://github.com/zed-industries/zed/assets/1486634/173b94b1-4d88-4333-b980-8fed937e6f6d

Release Notes:

- Added `experimental.theme_overrides` to `settings.json` to allow for
customizing the current theme.
  - This setting is experimental and subject to change.
2024-01-26 13:42:58 -05:00
Mikayla Maki
c9c9a6b11b Activate the nushell virtualenv overlay correctly (#6766)
The activate.nu file works a little differently than its counterparts in
other shells, and it needs to be invoked as an overlay, rather than
sourced directly into the shell.

This fixes an outstanding issue left over from #6323.


Release Notes:

- (Improved) ...
([#6323](https://github.com/zed-industries/zed/issues/6323)).
2024-01-26 10:38:21 -08:00
Conrad Irwin
8bc105ca1d Simplify Membership Management (#6747)
Simplify Zed's collaboration system by:
- Only allowing member management on root channels.
- Disallowing moving sub-channels between different roots.
- Disallowing public channels nested under private channels.

This should make the mental model easier to understand, and makes it
clearer
who has what access. It is also significantly simpler to implement, and
so
hopefully more performant and less buggy.

Still TODO:
- [x] Update collab_ui to match.
- [x] Fix channel buffer tests.

Release Notes:

- Simplified channel membership management.
2024-01-26 11:17:16 -07:00
Remco Smits
ac05853f2c Add readonly keyword to keyword list 2024-01-26 19:16:56 +01:00
Remco Smits
b5c64a128e Feature: PHP allow single quote autocompletion (#6796)
Release Notes:

- Added support for single quote autocompletion in PHP
([#6794](https://github.com/zed-industries/zed/issues/6794)).
- Added `#` to supported line comments for PHP
([#6794](https://github.com/zed-industries/zed/issues/6794)).
2024-01-26 19:16:35 +01:00
Conrad Irwin
22034d2083 Superstitiously wait some more for flakey test 2024-01-26 10:22:51 -07:00
Conrad Irwin
08de0d88b1 Update channel moving to match 2024-01-26 10:07:51 -07:00
Conrad Irwin
c6d33d4bb9 Only allow Manage Members on root channels 2024-01-26 09:40:41 -07:00
Thorsten Ball
3c3cdecdff Fix bracket captures for Go, C, C++, and Zig (#6784)
This fixes #6702 by adding the captures for Go, C, C++, and Zig.

Witnessed-by: @dammerung2718  <dammerung2718@icloud.com>

Release Notes:

- Fixed matching-bracket navigation via `%` in Vim mode not working for
`(` in Go, C, C++, and not working for any in Zig.
([#6702](https://github.com/zed-industries/zed/issues/6702))
2024-01-26 17:38:29 +01:00
Thorsten Ball
f537be4704 Fix bracket captures for Go, C, C++, and Zig
This fixes #6702 by adding the captures for Go, C, C++, and Zig.

Witness-by: David <dammerung2718@icloud.com>
2024-01-26 16:45:45 +01:00
Marshall Bowers
8da5e57d6d Factor out URLs in feedback crate (#6776)
This PR factors out the inlined URLs in the `feedback` crate so that
they don't mess with `rustfmt`.

Release Notes:

- N/A
2024-01-26 09:15:13 -05:00
Thomas Coratger
47860a5dc8 Combine observe_new_views into one in feedback crate (#6755)
Combine two `observe_new_views` calls into a single one in `init`
function of feedback crate.

Release Notes:

- N/A
2024-01-26 09:06:44 -05:00
George Munyoro
14322c8365 Fix grammatical error in contributing guidelines (#6772)
Release Notes:

- N/A
2024-01-26 14:53:03 +02:00
Tristam MacDonald
3847762d8c Activate the nushell virtualenv overlay correctly
The activate.nu file works a little differently than it's counterparts in other shells, and it needs to be invoked as an overlay, rather than sourced directly into the shell.

This fixes an outstanding issues left over from #6323.
2024-01-26 12:52:28 +01:00
Thorsten Ball
802405f6bc Fix panic when typing umlauts in command palette using Vim mode (#6761)
Release Notes:

- This fixes a panic that occurs when someone was using Vim mode and
typing umlauts into the command palette. E.g: `:%s/impërt`
2024-01-26 11:58:10 +01:00
Thorsten Ball
252e7e9467 Upgrade alacritty_terminal in hopes to avoid PTY poll failing (#6715)
We saw stack traces in our #panic channel pop up that failed on this
line:


3330614219/alacritty_terminal/src/event_loop.rs (L323-L324)

With this message:

thread 'PTY reader' panicked at 'called `Result::unwrap()` on an `Err`
value: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor"
}'

/Users/administrator/.cargo/git/checkouts/alacritty-afea874b09a502a5/3330614/alacritty_terminal/src/event_loop.rs:324

We don't know how to reproduce the error. It doesn't seem related to the
number of open PTY handles, because `openpty` itself didn't fail. We can
only assume that something went wrong between `openpty` and the setup of
the polling.

Since Alacritty itself changed its polling mechanism significantly by
switching from `mio` to `polling`
(https://github.com/alacritty/alacritty/pull/6846) we upgraded with the
hope that this will fix the bug.


Release Notes:

- Upgraded alacritty_terminal to newest version in order to hopefully
fix a rare panic that can occur when starting a new terminal.
2024-01-26 10:58:39 +01:00
Thorsten Ball
8d2a401f09 Fix panic when typing umlauts in command palette using Vim mode
Co-Authored-By: Antonio <antonio@zed.dev>
2024-01-26 10:57:14 +01:00
Thorsten Ball
698108ac8b Setup env correctly after upgrading alacritty_terminal
Co-Authored-By: Antonio <antonio@zed.dev>
2024-01-26 10:42:07 +01:00
Thorsten Ball
0aa1b1f070 Remove unused import 2024-01-26 09:38:06 +01:00
Thorsten Ball
c4cf5f2b2c Upgrade alacritty_terminal in hopes to avoid PTY poll failing
We saw stack traces in our #panic channel pop up that failed on this line:

    3330614219/alacritty_terminal/src/event_loop.rs (L323-L324)

With this message:

    thread 'PTY reader' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" }'
    /Users/administrator/.cargo/git/checkouts/alacritty-afea874b09a502a5/3330614/alacritty_terminal/src/event_loop.rs:324

We don't know how to reproduce the error. It doesn't seem related to the number of open PTY handles,
because `openpty` itself didn't fail. We can only assume that something went wrong between
`openpty` and the setup of the polling.

Since Alacritty itself changed its polling mechanism significantly by switching
from `mio` to `polling` (https://github.com/alacritty/alacritty/pull/6846) we upgraded
with the hope that this will fix the bug.

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Federico <code@fdionisi.me>
Co-authored-by: David <dammerung2718@icloud.com>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-01-26 09:38:05 +01:00
Bjerg
f7a2118b15 chore: remove unused deps in color crate (#6745) 2024-01-26 09:59:03 +02:00
Conrad Irwin
abdf302367 de-lint 2024-01-25 23:33:18 -07:00
Conrad Irwin
4b672621d3 Aggressively simplify channel permissions:
- Only allow setting permissions on the root channel
- Only allow public channels to be children of public channels
2024-01-25 23:26:32 -07:00
Nathan Sobo
1734403aa3 Add Zig support (#6690)
This PR adds support for Zig with the official LSP (ZLS) and the
treesitter grammar listed on the
[website](https://tree-sitter.github.io/tree-sitter/). I've tested this
on a few large codebases and go to definition, completion, and hover
overlays all seem to work fine with no error logs.

![Screenshot 2024-01-24 at 20 17
18](https://github.com/zed-industries/zed/assets/11731580/be7b4f1d-878e-4e3e-839b-a5f962df7555)

The highlights are adapted from the tree sitter repository which target
neovim I think, I've fixed a handful of issues and the highlighting is
looking pretty good but need to do more testing.

Release Notes:
- Added Zig support with zls
[#5300](https://github.com/zed-industries/zed/issues/5300).
2024-01-25 22:24:37 -07:00
Allan Calix
742329ee8f Highlight a broader range of zig functions 2024-01-25 20:30:07 -08:00
Allan Calix
ebbfff5ce8 Updates zigs highlight to emit right captures 2024-01-25 19:17:18 -08:00
Conrad Irwin
204af431e0 Vim :% and :0 (#6739)
Fixes #4303

Release Notes:

- Added :$ and :0 to vim command
([#4303](https://github.com/zed-industries/zed/issues/4303)).
2024-01-25 19:23:55 -07:00
Conrad Irwin
d519fc6b02 Vim :% and :0
Fixes #4303
2024-01-25 19:15:45 -07:00
Allan Calix
e5b71cc6ac Add zig support 2024-01-25 18:04:04 -08:00
0x29a
37647a67a7 ci: Bump actions/checkout to v4 (#6736)
change:

- bump actions/checkout version

Release Notes:

- N/A
2024-01-25 21:03:23 -05:00
Conrad Irwin
9820895284 Try to flesh out our panic stacks more (#6683)
Although we now usually get the .ips file alongside a panic, when we
don't it's much harder to track down where panics happen because we're
not showing inlined frames.

Try iterating over all symbols in a frame to see if that improves panic
reporting.

Release Notes:

- N/A
2024-01-25 18:57:45 -07:00
Lino Le Van
81aac492bd Add Deno LSP support (#5816)
This PR adds support for the deno LSP. Should be reviewable now.

Release Notes:

- Added support for the Deno LSP
([#5361](https://github.com/zed-industries/zed/issues/5361)).
2024-01-25 17:44:51 -08:00
Marshall Bowers
50b9e5d8d2 Add Gleam support (#6733)
This PR adds support for [Gleam](https://gleam.run/).

<img width="1320" alt="Screenshot 2024-01-25 at 6 39 18 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7891b6e9-d7dc-46a0-b7c5-8aa7854c1f35">

<img width="757" alt="Screenshot 2024-01-25 at 6 39 37 PM"
src="https://github.com/zed-industries/zed/assets/1486634/f7ce6b3f-6175-45cb-8547-cfd286d918c6">

<img width="694" alt="Screenshot 2024-01-25 at 6 39 55 PM"
src="https://github.com/zed-industries/zed/assets/1486634/b0838027-c377-47e6-bdd1-bdc9b67a8672">

There are still some areas of improvement, like extending what
constructs we support in the outline view, but this is a good start.

Release Notes:

- Added Gleam support
([#5162](https://github.com/zed-industries/zed/issues/5162)).
2024-01-25 18:44:35 -05:00
Conrad Irwin
716221cd38 Simplify handling of syncing versions
Currently whenever a channel changes we send a huge amount of data to
each member. This is the first step in reducing that

Co-Authored-By: Max <max@zed.dev>
Co-Authored-By: bennetbo <bennetbo@gmx.de>
2024-01-25 15:41:53 -07:00
Marshall Bowers
20c90f07e1 Clean up references in doc comments in language crate (#6729)
This PR cleans up a handful of references in doc comments in the
`language` crate so that `rustdoc` will link and display them correctly.

Release Notes:

- N/A
2024-01-25 16:04:58 -05:00
Max Brunsfeld
039ef1ad5a Put back Project's completion-documention handler (#6451)
This restores the ability for project guests to see documentation on
autocomplete suggestions.

@ForLoveOfCats This code might have gotten lost during the GPUI upgrade.
I'm not sure what happened. I tested this locally, and it seems to fix
completion docs for guests. Could you 👀 and see if there are any tests
or any other code that got lost along with this during the upgrade?
2024-01-25 12:46:51 -08:00
Conrad Irwin
b72c037199 TEMP 2024-01-25 13:24:45 -07:00
Nathan Sobo
9fd0938eb1 Github => GitHub (#6727)
🔥 that this is open source now.

Release Notes:

- Changed instances of "Github" in user-facing UI to CamelCase "GitHub".
2024-01-25 12:29:51 -07:00
Chris Wanstrath
b5fc91e455 Github => GitHub 2024-01-25 11:23:28 -08:00
Marshall Bowers
a3d431d86f Remove placeholder description from PR template (#6726)
This PR removes the placeholder description from the PR template, opting
to just leave empty space instead.

I've seen lots of instances where authors will not delete the
placeholder, and it ends up in Git history, which is not desirable.

Release Notes:

- N/A
2024-01-25 14:19:22 -05:00
Mikayla Maki
43060b2d13 Update contributing (#6725)
[[PR Description]]

Release Notes:

- (Added|Fixed|Improved) ...
([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
2024-01-25 11:11:54 -08:00
Mikayla
0457ad3f6e Update contributing 2024-01-25 11:11:28 -08:00
SweetPPro
006e003698 Add syntax highlighting for .htm and .shtml files (#6705)
enabled syntax highlighting for **.htm** and **.shtml** files

Release Notes:
- Added support for syntax highlighting in HTML files with `.htm` and
`.shtml` extensions
([#4510](https://github.com/zed-industries/zed/issues/4510)).
2024-01-25 14:05:17 -05:00
Mikayla Maki
b5fa5beee4 Fix typos discovered by codespell (#6718)
> codespell --skip="*.json,*.rtf" \

--ignore-words-list=crate,delet,inout,iterm,jumo,lightening,othe,ser,tabe,te,tese,thi,updat
* https://github.com/codespell-project/codespell/blob/master/README.rst

Release Notes:

- N/A
2024-01-25 10:57:18 -08:00
Marshall Bowers
6103f67875 Make follower avatars smaller (#6724)
This PR makes the avatars of followers in a facepile smaller than the
leader's avatar.

<img width="227" alt="Screenshot 2024-01-25 at 1 42 14 PM"
src="https://github.com/zed-industries/zed/assets/1486634/defc22b4-4ae1-4d63-a0d8-53e3ca8cce04">

Release Notes:

- Adjusted the size of follower avatars to be smaller than the leader.

Co-authored-by: Conrad <conrad@zed.dev>
2024-01-25 13:53:50 -05:00
Mikayla Maki
3d898c562e Update crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql
Co-authored-by: Christian Clauss <cclauss@me.com>
2024-01-25 10:50:59 -08:00
Piotr Osiewicz
e9edad1d51 language: Accept multiple values in line_comment language knob. (#6713)
This opens up a possibility of supporting multiple comment continuation
flavours in editor, e.g. doc comments for Rust (which we seize as well
in this commit). Only the first `line_comment` value is used for
Editor::ToggleComments

Fixes: https://github.com/zed-industries/zed/issues/6692

Release Notes:
- Added support for doc-comment continuations in Rust language.
2024-01-25 19:25:21 +01:00
Conrad Irwin
dd25902aeb collab 0.41.0 2024-01-25 11:17:09 -07:00
Conrad Irwin
fc2f5d86c7 collab fixes (#6720)
- Fail faster on serialization failure
- Move expensive participant update out of transaction

Release Notes:

- Fixed creating/moving channels in busy workspaces
2024-01-25 11:16:19 -07:00
Conrad Irwin
fbdca993ff Format 2024-01-25 11:12:02 -07:00
Conrad Irwin
adb6f3e9f7 Move expensive participant update out of transaction
Co-Authored-By: Marshall <marshall@zed.dev>
2024-01-25 11:05:19 -07:00
Christian Clauss
cff2e8bbe0 Fix typos discovered by codespell 2024-01-25 18:32:32 +01:00
Conrad Irwin
ca27ac21c2 Fail faster on serialization failure
Co-Authored-By: Thorsten <thorsten@zed.dev>
2024-01-25 09:59:57 -07:00
Marshall Bowers
7068161bd7 Sort .mailmap alphabetically (#6711)
This PR sorts the entries in the `.mailmap` file to keep them in
alphabetical order.

Release Notes:

- N/A
2024-01-25 09:51:47 -05:00
Kirill Bulatov
2b844f5cb5 Sort file finder matched entries better (#6704)
* applicable history items were sorted by latest opened order, now
sorted by match score as the search matches
* adjust the match sorting to show paths in the alphanumerical order (in
case of a tie on other params)

Release Notes:

- Improved file finder entries ordering

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-01-25 15:25:33 +02:00
Thorsten Ball
50c3ad963e Improve performance of select-all-matches (#6700)
This fixes #6440.

The previous approach was calling select-next-match in a loop, which
leaves optimizations on the table when you already know that you want to
select all of the matches.

So what we did here is to optimize the code for the "give me all
matches" case:

1. Find all results in the current buffer
2. Build up *all* selections
3. Sort selections & throw away overlapping ones (keep oldest)
4. Unfold if necessary
5. Render selections

On my M3 Max searching for `<span` in the test file [1] from the ticket,
it

previously took: ~1.07s
now takes: ~4ms

[1]:
https://github.com/standardebooks/edgar-allan-poe_poetry/blob/master/src/epub/text/poetry.xhtml

![screenshot-2024-01-25-12 49
32@2x](https://github.com/zed-industries/zed/assets/1185253/9f8ef0fa-a3a7-461c-9ed6-263e48835806)

### Release Notes:

- Improved performance of select-all-matches by factor of ~250
([#6440](https://github.com/zed-industries/zed/issues/6440)).
2024-01-25 13:59:46 +01:00
Thomas Coratger
e13fb31287 small refactoring in color crate (#6695)
Make the code more readable
2024-01-25 14:36:52 +02:00
Thorsten Ball
0a78c67647 Improve performance of select-all-matches
This fixes #6440.

The previous approach was calling select-next-match in a loop, which
leaves optimizations on the table when you already know that you want to
select all of the matches.

So what we did here is to optimize the code for the "give me all
matches" case:

1. Find all results in the current buffer
2. Build up *all* selections
3. Sort selections & throw away overlapping ones (keep oldest)
4. Unfold if necessary
5. Render selections

On my M3 Max searching for `<span` in the test file [1] from the ticket,
it

previously took: ~1.07s
now takes: ~4ms

[1]: https://github.com/standardebooks/edgar-allan-poe_poetry/blob/master/src/epub/text/poetry.xhtml

Co-authored-by: Antonio <antonio@zed.dev>
2024-01-25 13:35:22 +01:00
Piotr Osiewicz
062288dea5 Search woes (#6698)
Fixes #6441 
Release Notes:
- Fixed "SelectNextMatch", "SelectPrevMatch" and "SelectAllMatches"
actions not working when search bar is not visible.
- Fixed "SelectPrevMatch" not being bound in project search.

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-01-25 13:28:21 +01:00
Thorsten Ball
dd3ec15acc Log error if worktree fails to relativize git repo path (#6693)
This is a follow-up to #6459. It logs the error instead of silently
skipping it.

Release Notes:

- N/A
2024-01-25 11:27:50 +01:00
Thorsten Ball
d17d37ff61 Log error if worktree fails to relativize git repo path
We saw a panic that was caused by the previous `Option.unwrap()`, so
this changes the method to return a `Result` and logs the error if
possible.

Co-authored-by: Antonio <antonio@zed.dev>
2024-01-25 11:10:00 +01:00
Thorsten Ball
710e47977d Add Conrad and Thorsten to .mailmap file (#6691)
Follow-up to #4138.

Release Notes:

- N/A
2024-01-25 10:08:01 +01:00
Thorsten Ball
c6e7cf1cbc bugfix: fix LineEnding for windows (#6688)
fixes a bug when compiling on windows


![image](https://github.com/zed-industries/zed/assets/715417/5a35b1b2-29f4-4987-9410-730c9b287f82)

Release Notes:

- Fixed: compilation error related to `LineEnding` on Windows
2024-01-25 09:58:53 +01:00
Thorsten Ball
dcf05812c2 Add Conrad and Thorsten to .mailmap file 2024-01-25 09:56:14 +01:00
Sharun
0c4679f892 bugfix: fix LineEnding for windows 2024-01-25 03:16:42 -05:00
gmorenz
dd07d2f8a2 Update cocoa to crates.io version (#6452)
https://github.com/servo/core-foundation-rs/pull/457 was released in
cocoa 0.25, so this patch dependency on github can be removed.

Release Notes:

- N/A
2024-01-25 10:04:29 +02:00
Conrad Irwin
5c1de4ce26 Try to flesh out our panic stacks more
Although we now usually get the .ips file alongside a panic, when we
don't it's much harder to track down where panics happen because we're
not showing inlined frames.

Try iterating over all symbols in a frame to see if that improves panic
reporting.
2024-01-24 23:33:54 -07:00
Conrad Irwin
1c2859d72b collab errors (#4152)
One of the complaints of users on our first Hack call was that the error
messages you got when channel joining failed were not great.

This aims to fix that specific case, and lay the groundwork for future
improvements.

It adds two new methods to anyhow::Error

* `.error_code()` which returns a value from zed.proto (or
ErrorCode::Internal if the error has no specific tag)
* `.error_tag("key")` which returns the value of the tag (or None).

To construct errors with these fields set, you can use a builder API
based on the ErrorCode type:

* `Err(ErrorCode::Forbidden.anyhow())`
* `Err(ErrorCode::Forbidden.message("cannot join channel").into())` - to
add any context you want in the logs
* `Err(ErrorCode::WrongReleaseChannel.tag("required", "stable").into())`
- to add structured metadata to help the client handle the error better.


Release Notes:

- Improved error messaging when channel joining fails.
2024-01-24 23:23:58 -07:00
Conrad Irwin
01424a62ea Allow prompts to have detail, and use for good
Make channel panel errors louder
2024-01-24 23:15:37 -07:00
Sean Aye
5fcc75be1f gpui: Add hello world example (#6465)
Added hello world example to the gpui crate copied from https://gpui.rs

Release Notes:

- N/A
2024-01-25 01:03:53 -05:00
Conrad Irwin
cf3b2ba146 Fix panic from unwrapping a relativize() (#6459)
Release Notes:

- Fixed a panic that could occur finding a git repository.
2024-01-24 23:03:41 -07:00
Joseph T. Lyons
e77db87bad Remove references to community repo (#6466)
Release Notes:

- N/A
2024-01-25 00:38:01 -05:00
Joseph T. Lyons
f9170cb239 Remove references to community repo 2024-01-25 00:30:26 -05:00
Marshall Bowers
bf7489269e Clean up docs (#6464)
This PR cleans up some of the Zed meta docs.

Namely:
- Removed references to the `zed-industries/community` repository
- Removed old docs directory (it'll be in Git history if we need it)

Release Notes:

- N/A
2024-01-25 00:05:15 -05:00
Jeff Li
ba97661e1c Fix backend dependencies link in local collaboration docs (#6461)
Release Notes:

- N/A
2024-01-24 23:53:41 -05:00
Conrad Irwin
320088f1fa Fix panic from unwrapping a relativize() 2024-01-24 20:56:39 -07:00
Conrad Irwin
df420c3767 Better naming 2024-01-24 20:48:29 -07:00
Conrad Irwin
4bcd3494b7 Try to send typed errors back and forth
TEMP

TEMP

First pass of structured errors

Improved error handling for channel joining failures
2024-01-24 20:32:39 -07:00
Conrad Irwin
865369882e Fix circular locking in prompts (#6456)
Sometimes Cocoa calls app delegate methods (notably the display link)
while we're calling Cocoa methods. This causes a deadlock unless we
are careful to run cocao methods while we're not holding our internal
locks

Release Notes:

- Fixed a crash when opening the MacOS Save As dialogue.
2024-01-24 20:10:32 -07:00
Conrad Irwin
a181dc8d58 Use the correct snapshot when calculating mouse positions (#6453)
Release Notes:

- Fixed a panic in calculating remote cursor positions
2024-01-24 19:44:02 -07:00
Conrad Irwin
249a6da54a Fix circular locking in prompts
Sometimes Cocoa calls app delegate methods (notably the display link)
while we're calling Cocoa methods. This causes a deadlock unless we
are careful to run cocao methods while we're not holding our internal
locks
2024-01-24 19:40:01 -07:00
Conrad Irwin
f185aca25a Use the correct snapshot when calculating mouse positions 2024-01-24 19:10:53 -07:00
Max Brunsfeld
6ed7cc7833 Simplify language server startup (#6449)
These are just some small refactorings of our language-server-starting
code, motivated by another change that I decided to bail on:
https://github.com/zed-industries/zed/pull/6448.
2024-01-24 17:36:50 -08:00
Marshall Bowers
90c1d8f734 Update Cargo.lock (#6445)
This PR updates `Cargo.lock` to reflect the changes made in #6435.

Release Notes:

- N/A
2024-01-24 17:46:40 -05:00
Dragan Okanovic
416696a686 Fix typo in build instructions (#6444)
Typo fix.

Release Notes:

- N/A
2024-01-24 17:36:21 -05:00
Joseph T. Lyons
10437794e4 Remove unused dependency (#6435)
[[PR Description]]

The `color` crate is not actually used by the `zed` crate, despite being
listed as a dependency. This PR removes it.
2024-01-24 16:19:04 -05:00
Julia
569bb687be Prevent channel from vertically growing when containing facepile (#6439)
Previously at UI font size of 20
 - User-less channel: 28 px tall
 - Occupied channel: 30 px tall
 
Now, still at UI font size 20:
 - User-less channel: 30 px tall
 - Occupied channel: 30 px tall

Release Notes:

- Fixed an issue where a channel would grow in height while showing
participant avatars.
2024-01-24 16:17:25 -05:00
Julia
9bc968eabb Prevent channel from vertically growing when containing facepile 2024-01-24 15:57:12 -05:00
Conrad Irwin
4ac3095a15 chat mentions for newly joined users (#5099)
Release Notes:

- Added chat mentions for everyone in the call
2024-01-24 13:33:23 -07:00
Julian Braha
5907bb5b55 Remove unused dependency 2024-01-24 20:29:29 +00:00
Conrad Irwin
28b2c89254 Fix crash in feedback modal (#6431)
After the general release we saw a number of crashes due to a SEGFAULT
inside the
System::new() method apparently relating to refreshing the user list.

As we do not need the user list, and the similar code in the telemtry
create is not crashing,
do less work for now.

Release Notes:

- Fixed a crash when opening the feedback modal
2024-01-24 13:25:22 -07:00
Chase Weaver
cf3b4b0ba7 Fix: Comment shortcut in Svelte files now respects if the line(s) are HTML vs JS/TS (#5860)
Release Notes:

- Fixed: When using the comment shortcut, previously HTML or JS/TS would
all prefix lines with `// `. This PR brings the comments inline with
what is expected (`// ` for JS/TS, `<!-- ... -->` for HTML). Fixes
[#4578](https://github.com/zed-industries/zed/issues/4578).
2024-01-24 12:24:49 -08:00
Mikayla Maki
71ec781215 Fix RUSTSEC-2018-0017 (#5338)
[[PR Description]]

This replaces the `tempdir` crate with `tempfile`, which resolves
[RUSTSEC-2018-0017](https://rustsec.org/advisories/RUSTSEC-2018-0017).
2024-01-24 12:19:05 -08:00
Joseph T. Lyons
e1b7b5eaa6 Update all links pointing to community (#6434)
Release Notes:

- N/A
2024-01-24 15:17:47 -05:00
Joseph T. Lyons
40dbe15b2a Update all links point to community 2024-01-24 15:11:17 -05:00
Joseph T. Lyons
6c555fe13c Fix typos (#6429)
Release Notes:

- N/A
2024-01-24 15:02:03 -05:00
Conrad Irwin
334dc620ea Fix crash in feedback modal 2024-01-24 13:00:12 -07:00
Joseph T. Lyons
1a11da916b Fix typos 2024-01-24 14:58:47 -05:00
Joseph T. Lyons
fc01eeebbc Update links to script 2024-01-24 14:48:44 -05:00
Joseph T. Lyons
fde4c09906 Remove open source checklist from issue config 2024-01-24 14:45:17 -05:00
Conrad Irwin
482c01aaf3 Ensure chat opens when guests join shared projects (#6425)
This was broken because the panel was created before being added to a
dock. Invert the control order and add `starts_open()` to the Panel
trait (which lets us simplify how this worked for the ProjectPanel too)

Release Notes:

- Improved Chat visibility when joining a call as a guest
2024-01-24 12:45:04 -07:00
Joseph T. Lyons
db33eafdb1 Delete 0_feature_parity_report.yml 2024-01-24 14:44:00 -05:00
Joseph T. Lyons
855e0f6f36 Add top-ranking issues script 2024-01-24 14:40:55 -05:00
Conrad Irwin
c56debc705 clippy 2024-01-24 12:40:24 -07:00
Conrad Irwin
e7db5d0638 Use run_until_parked instead of condition
Avoids spurious failures when the CI server is going slow
2024-01-24 12:28:45 -07:00
Conrad Irwin
a860ca6a3c Fix tests that were toggling the panel shut 2024-01-24 12:22:12 -07:00
Conrad Irwin
4427e7968b (probably) install rosetta if it's not there (#5817)
We set up a new CI server recently, and this caused a build to fail

Release Notes:

- N/A
2024-01-24 12:17:02 -07:00
Conrad Irwin
2a11c22760 Ensure chat opens when guests join shared projects
This was broken because the panel was created before being added to a
dock. Invert the control order and add `starts_open()` to the Panel
trait.
2024-01-24 12:06:03 -07:00
charlotte
6285decfa2 Add Git submodule initialization to repository setup steps (#5818)
Was running into an issue building live_kit_server after a fresh clone
due to missing dependencies for build.rs. The use of git submodules
wasn't currently documented.
2024-01-24 14:02:26 -05:00
Mikayla Maki
4599fa840d Revert "Ensure that notify observations are sent during Window::draw()" (#6152)
Reverts zed-industries/zed#4236

This causes an infinite loop when opening the language server logs
2024-01-24 10:56:45 -08:00
Pseudomata
da01c1a83b Open bug reports in zed-industries/zed (#5862)
It looks like bug reports are being moved from
`zed-industries/community` to `zed-industries/zed`. This PR updates the
`feedback` crate to update the GitHub URL to reflect this change.
2024-01-24 13:51:43 -05:00
Mikayla Maki
a7368904f3 Revert "Ensure that notify observations are sent during Window::draw()" 2024-01-24 10:50:52 -08:00
Conrad Irwin
ad537f638c (probably) install rosetta if it's not there 2024-01-24 11:35:59 -07:00
Conrad Irwin
e072c96003 Fix tests 2024-01-24 11:35:10 -07:00
Conrad Irwin
9693e14809 Merge branch 'main' into chat-mentions-for-newly-joined-users 2024-01-24 11:31:27 -07:00
Patrick Dubroy
291f353085 Fix registers link in README.md (#5389)
It looked to me that `registers` was intended to be a link here...if I'm
wrong, feel free to ignore :-)

Release Notes:

- N/A
2024-01-24 13:21:08 -05:00
Julian Braha
08d2ba72d6 Add missing .unwrap() 2024-01-24 18:19:03 +00:00
Conrad Irwin
c5ad1728f9 Clippy 2024-01-24 11:18:17 -07:00
Julian Braha
85f5e7d0bb Fix formatting with cargo-fmt 2024-01-24 18:10:11 +00:00
Julian Braha
fd6f71d287 Replace tempdir crate with tempfile 2024-01-24 17:58:09 +00:00
Conrad Irwin
c1df166700 Allow completions of everyone in the call 2024-01-24 10:51:46 -07:00
Max Brunsfeld
c81d318098 Start work on allowing mentions for all users in call 2024-01-24 09:45:26 -08:00
Joseph T. Lyons
f8604e88ef Add GitHub items 2024-01-24 12:26:15 -05:00
Conrad Irwin
489ef23b76 Show hovered cursors less flickerily (#4242)
Now when hovering on a cursor it'll stay around for 2 seconds

Release Notes:

- Improved hovering over collaborators' cursors.
2024-01-24 10:20:34 -07:00
Joseph T. Lyons
a4897e00b4 v0.121.x dev 2024-01-24 10:54:33 -05:00
Conrad Irwin
4e085b2052 Show hovered cursors less flickerily 2024-01-23 22:32:22 -07:00
208 changed files with 5519 additions and 3415 deletions

View File

@@ -0,0 +1,24 @@
name: Feature Request
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
labels: ["admin read", "triage", "enhancement"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: |
If applicable, add mockups / screenshots to help present your vision of the feature
description: Drag images into the text input below
validations:
required: false

View File

@@ -0,0 +1,47 @@
name: Language Support
description: Request language support
title: "<name_of_language> support"
labels:
[
"admin read",
"triage",
"enhancement",
"language",
"unsupported language",
"potential plugin",
]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: input
attributes:
label: Language
description: What language do you want support for?
placeholder: HTML
validations:
required: true
- type: input
attributes:
label: Tree Sitter parser link
description: If applicable, provide a link to the appropriate tree sitter parser. Look here first - https://tree-sitter.github.io/tree-sitter/#available-parsers
placeholder: https://github.com/tree-sitter/tree-sitter-html
validations:
required: false
- type: input
attributes:
label: Language server link
description: If applicable, provide a link to the appropriate language server. Look here first - https://microsoft.github.io/language-server-protocol/implementors/servers/
placeholder: https://github.com/Microsoft/vscode/tree/main/extensions/html-language-features/server
validations:
required: false
- type: textarea
attributes:
label: Misc notes
description: Provide any additional things the team should consider when adding support for this language
validations:
required: false

38
.github/ISSUE_TEMPLATE/2_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Bug Report
description: "Tip: open this issue template from within Zed with the `file bug report` command palette action"
labels: ["admin read", "triage", "defect"]
body:
- type: checkboxes
attributes:
label: Check for existing issues
description: Check the backlog of issues to reduce the chances of creating duplicates; if an issue already exists, place a `+1` (👍) on it.
options:
- label: Completed
required: true
- type: textarea
attributes:
label: Describe the bug / provide steps to reproduce it
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below.
validations:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea
attributes:
label: |
If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
description: Drag Zed.log into the text input below
validations:
required: false

10
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
contact_links:
- name: Top-Ranking Issues
url: https://github.com/zed-industries/zed/issues/5393
about: See an overview of the most popular Zed issues
- name: Platform Support
url: https://github.com/zed-industries/zed/issues/5391
about: A quick note on platform support
- name: Postive Feedback
url: https://github.com/zed-industries/zed/discussions/5397
about: A central location for kind words about Zed

View File

@@ -1,5 +1,5 @@
[[PR Description]]
Release Notes:
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
on:
schedule:
- cron: "0 */12 * * *"
workflow_dispatch:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r script/update_top_ranking_issues/requirements.txt
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --prod

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ DerivedData/
.netrc
.swiftpm
**/*.db
.pytest_cache
.venv

View File

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

View File

@@ -4,14 +4,14 @@ Thanks for your interest in contributing to Zed, the collaborative platform that
We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome.
All activity in Zed forums is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas
If you're looking for ideas about what to work on, check out:
- Our public roadmap (link coming soon!) contains a rough outline of our near-term priorities for Zed.
- Our [top-ranking issues](https://github.com/zed-industries/community/issues/52) based on votes by the community.
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
Outside of a handful of extremely popular languages and themes, we are generally not looking to extend Zed's language or theme support by directly building them into Zed. We really want to build a plugin system to handle making the editor extensible going forward. If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.
@@ -37,8 +37,9 @@ We plan to set aside time each week to pair program with contributors on promisi
- Add test coverage and documentation
- Choose tasks that align with our roadmap
- Pair with us and watch us code to learn the codebase
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
## Bird-eye's view of Zed
## Bird's-eye view of Zed
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:

509
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -29,4 +29,4 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
tempdir.workspace = true
tempfile.workspace = true

View File

@@ -130,7 +130,8 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
));
}
@@ -291,7 +292,9 @@ impl AutoUpdater {
cx.notify();
})?;
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
let temp_dir = tempfile::Builder::new()
.prefix("zed-auto-update")
.tempdir()?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let running_app_path = ZED_APP_PATH

View File

@@ -61,11 +61,12 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let buffer = cx.new_model(|_| {
let buffer = cx.new_model(|cx| {
let capability = channel_store.read(cx).channel_capability(channel.id);
language::Buffer::remote(
response.buffer_id,
response.replica_id as u16,
channel.channel_buffer_capability(),
capability,
base_text,
)
})?;

View File

@@ -13,11 +13,11 @@ use gpui::{
};
use language::Capability;
use rpc::{
proto::{self, ChannelVisibility},
proto::{self, ChannelRole, ChannelVisibility},
TypedEnvelope,
};
use std::{mem, sync::Arc, time::Duration};
use util::{async_maybe, ResultExt};
use util::{async_maybe, maybe, ResultExt};
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let channel_store =
@@ -29,33 +29,47 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub type ChannelId = u64;
#[derive(Debug, Clone, Default)]
struct NotesVersion {
epoch: u64,
version: clock::Global,
}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
client: Arc<Client>,
user_store: Model<UserStore>,
_rpc_subscription: Subscription,
_rpc_subscriptions: [Subscription; 2],
_watch_connection_status: Task<Option<()>>,
disconnect_channel_buffers_task: Option<Task<()>>,
_update_channels: Task<()>,
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct Channel {
pub id: ChannelId,
pub name: SharedString,
pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>,
pub unseen_message_id: Option<u64>,
pub parent_path: Vec<u64>,
}
#[derive(Default)]
pub struct ChannelState {
latest_chat_message: Option<u64>,
latest_notes_versions: Option<NotesVersion>,
observed_chat_message: Option<u64>,
observed_notes_versions: Option<NotesVersion>,
role: Option<ChannelRole>,
}
impl Channel {
pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned()
@@ -65,6 +79,17 @@ impl Channel {
+ &self.id.to_string()
}
pub fn is_root_channel(&self) -> bool {
self.parent_path.is_empty()
}
pub fn root_id(&self) -> ChannelId {
self.parent_path
.first()
.map(|id| *id as ChannelId)
.unwrap_or(self.id)
}
pub fn slug(&self) -> String {
let slug: String = self
.name
@@ -74,14 +99,6 @@ impl Channel {
slug.trim_matches(|c| c == '-').to_string()
}
pub fn channel_buffer_capability(&self) -> Capability {
if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
Capability::ReadWrite
} else {
Capability::ReadOnly
}
}
}
pub struct ChannelMembership {
@@ -100,8 +117,7 @@ impl ChannelMembership {
},
kind_order: match self.kind {
proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::AncestorMember => 1,
proto::channel_member::Kind::Invitee => 2,
proto::channel_member::Kind::Invitee => 1,
},
username_order: self.user.github_login.as_str(),
}
@@ -137,8 +153,10 @@ impl ChannelStore {
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let rpc_subscription =
client.add_message_handler(cx.weak_model(), Self::handle_update_channels);
let rpc_subscriptions = [
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
];
let mut connection_status = client.status();
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
@@ -175,7 +193,7 @@ impl ChannelStore {
update_channels_tx,
client,
user_store,
_rpc_subscription: rpc_subscription,
_rpc_subscriptions: rpc_subscriptions,
_watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None,
_update_channels: cx.spawn(|this, mut cx| async move {
@@ -195,6 +213,7 @@ impl ChannelStore {
.await
.log_err();
}),
channel_states: Default::default(),
}
}
@@ -306,39 +325,16 @@ impl ChannelStore {
})
}
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
self.channel_index
.by_id()
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool {
self.channel_states
.get(&channel_id)
.map(|channel| channel.unseen_note_version.is_some())
.is_some_and(|state| state.has_channel_buffer_changed())
}
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
self.channel_index
.by_id()
pub fn has_new_messages(&self, channel_id: ChannelId) -> bool {
self.channel_states
.get(&channel_id)
.map(|channel| channel.unseen_message_id.is_some())
}
pub fn notes_changed(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_index.note_changed(channel_id, epoch, version);
cx.notify();
}
pub fn new_message(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_index.new_message(channel_id, message_id);
cx.notify();
.is_some_and(|state| state.has_new_messages())
}
pub fn acknowledge_message_id(
@@ -347,8 +343,23 @@ impl ChannelStore {
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_index
.acknowledge_message_id(channel_id, message_id);
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.acknowledge_message_id(message_id);
cx.notify();
}
pub fn update_latest_message_id(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.update_latest_message_id(message_id);
cx.notify();
}
@@ -359,9 +370,25 @@ impl ChannelStore {
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_index
.acknowledge_note_version(channel_id, epoch, version);
cx.notify();
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.acknowledge_notes_version(epoch, version);
cx.notify()
}
pub fn update_latest_notes_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_states
.entry(channel_id)
.or_insert_with(|| Default::default())
.update_latest_notes_version(epoch, version);
cx.notify()
}
pub fn open_channel_chat(
@@ -454,10 +481,42 @@ impl ChannelStore {
}
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
let Some(channel) = self.channel_for_id(channel_id) else {
return false;
};
channel.role == proto::ChannelRole::Admin
self.channel_role(channel_id) == proto::ChannelRole::Admin
}
pub fn is_root_channel(&self, channel_id: ChannelId) -> bool {
self.channel_index
.by_id()
.get(&channel_id)
.map_or(false, |channel| channel.is_root_channel())
}
pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
self.channel_index
.by_id()
.get(&channel_id)
.map_or(false, |channel| {
channel.visibility == ChannelVisibility::Public
})
}
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
match self.channel_role(channel_id) {
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
_ => Capability::ReadOnly,
}
}
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
maybe!({
let mut channel = self.channel_for_id(channel_id)?;
if !channel.is_root_channel() {
channel = self.channel_for_id(channel.root_id())?;
}
let root_channel_state = self.channel_states.get(&channel.id);
root_channel_state?.role
})
.unwrap_or(proto::ChannelRole::Guest)
}
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
@@ -508,7 +567,7 @@ impl ChannelStore {
pub fn move_channel(
&mut self,
channel_id: ChannelId,
to: Option<ChannelId>,
to: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
@@ -747,6 +806,36 @@ impl ChannelStore {
Ok(())
}
async fn handle_update_user_channels(
this: Model<Self>,
message: TypedEnvelope<proto::UpdateUserChannels>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
for buffer_version in message.payload.observed_channel_buffer_version {
let version = language::proto::deserialize_version(&buffer_version.version);
this.acknowledge_notes_version(
buffer_version.channel_id,
buffer_version.epoch,
&version,
cx,
);
}
for message_id in message.payload.observed_channel_message_id {
this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx);
}
for membership in message.payload.channel_memberships {
if let Some(role) = ChannelRole::from_i32(membership.role) {
this.channel_states
.entry(membership.channel_id)
.or_insert_with(|| ChannelState::default())
.set_role(role)
}
}
})
}
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.channel_index.clear();
self.channel_invitations.clear();
@@ -909,10 +998,7 @@ impl ChannelStore {
Arc::new(Channel {
id: channel.id,
visibility: channel.visibility(),
role: channel.role(),
name: channel.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel.parent_path,
}),
),
@@ -921,8 +1007,8 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
|| !payload.unseen_channel_messages.is_empty()
|| !payload.unseen_channel_buffer_changes.is_empty();
|| !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -963,20 +1049,19 @@ impl ChannelStore {
}
}
for unseen_buffer_change in payload.unseen_channel_buffer_changes {
let version = language::proto::deserialize_version(&unseen_buffer_change.version);
index.note_changed(
unseen_buffer_change.channel_id,
unseen_buffer_change.epoch,
&version,
);
for latest_buffer_version in payload.latest_channel_buffer_versions {
let version = language::proto::deserialize_version(&latest_buffer_version.version);
self.channel_states
.entry(latest_buffer_version.channel_id)
.or_default()
.update_latest_notes_version(latest_buffer_version.epoch, &version)
}
for unseen_channel_message in payload.unseen_channel_messages {
index.new_messages(
unseen_channel_message.channel_id,
unseen_channel_message.message_id,
);
for latest_channel_message in payload.latest_channel_message_ids {
self.channel_states
.entry(latest_channel_message.channel_id)
.or_default()
.update_latest_message_id(latest_channel_message.message_id);
}
}
@@ -1025,3 +1110,69 @@ impl ChannelStore {
}))
}
}
impl ChannelState {
fn set_role(&mut self, role: ChannelRole) {
self.role = Some(role);
}
fn has_channel_buffer_changed(&self) -> bool {
if let Some(latest_version) = &self.latest_notes_versions {
if let Some(observed_version) = &self.observed_notes_versions {
latest_version.epoch > observed_version.epoch
|| latest_version
.version
.changed_since(&observed_version.version)
} else {
true
}
} else {
false
}
}
fn has_new_messages(&self) -> bool {
let latest_message_id = self.latest_chat_message;
let observed_message_id = self.observed_chat_message;
latest_message_id.is_some_and(|latest_message_id| {
latest_message_id > observed_message_id.unwrap_or_default()
})
}
fn acknowledge_message_id(&mut self, message_id: u64) {
let observed = self.observed_chat_message.get_or_insert(message_id);
*observed = (*observed).max(message_id);
}
fn update_latest_message_id(&mut self, message_id: u64) {
self.latest_chat_message =
Some(message_id.max(self.latest_chat_message.unwrap_or_default()));
}
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
if let Some(existing) = &mut self.observed_notes_versions {
if existing.epoch == epoch {
existing.version.join(version);
return;
}
}
self.observed_notes_versions = Some(NotesVersion {
epoch,
version: version.clone(),
});
}
fn update_latest_notes_version(&mut self, epoch: u64, version: &clock::Global) {
if let Some(existing) = &mut self.latest_notes_versions {
if existing.epoch == epoch {
existing.version.join(version);
return;
}
}
self.latest_notes_versions = Some(NotesVersion {
epoch,
version: version.clone(),
});
}
}

View File

@@ -37,43 +37,6 @@ impl ChannelIndex {
channels_by_id: &mut self.channels_by_id,
}
}
pub fn acknowledge_note_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
if epoch > *unseen_epoch
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
{
channel.unseen_note_version = None;
}
}
}
}
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some(unseen_message_id) = channel.unseen_message_id {
if message_id >= unseen_message_id {
channel.unseen_message_id = None;
}
}
}
}
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
}
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
}
/// A guard for ensuring that the paths index maintains its sort and uniqueness
@@ -85,36 +48,25 @@ pub struct ChannelPathsInsertGuard<'a> {
}
impl<'a> ChannelPathsInsertGuard<'a> {
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(self.channels_by_id, channel_id, epoch, version);
}
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(self.channels_by_id, channel_id, message_id)
}
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
let mut ret = false;
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
let existing_channel = Arc::make_mut(existing_channel);
ret = existing_channel.visibility != channel_proto.visibility()
|| existing_channel.role != channel_proto.role()
|| existing_channel.name != channel_proto.name;
|| existing_channel.name != channel_proto.name
|| existing_channel.parent_path != channel_proto.parent_path;
existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name.into();
existing_channel.parent_path = channel_proto.parent_path.into();
} else {
self.channels_by_id.insert(
channel_proto.id,
Arc::new(Channel {
id: channel_proto.id,
visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path,
}),
);
@@ -153,32 +105,3 @@ fn channel_path_sorting_key<'a>(
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name)
}
fn insert_note_changed(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_version = Arc::make_mut(channel)
.unseen_note_version
.get_or_insert((0, clock::Global::new()));
if epoch > unseen_version.0 {
*unseen_version = (epoch, version.clone());
} else {
unseen_version.1.join(version);
}
}
}
fn insert_new_message(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
message_id: u64,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
*unseen_message_id = message_id.max(*unseen_message_id);
}
}

View File

@@ -19,14 +19,12 @@ fn test_update_channels(cx: &mut AppContext) {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: Vec::new(),
},
proto::Channel {
id: 2,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: Vec::new(),
},
],
@@ -38,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) {
&channel_store,
&[
//
(0, "a".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), proto::ChannelRole::Admin),
(0, "a".to_string()),
(0, "b".to_string()),
],
cx,
);
@@ -52,14 +50,12 @@ fn test_update_channels(cx: &mut AppContext) {
id: 3,
name: "x".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![1],
},
proto::Channel {
id: 4,
name: "y".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![2],
},
],
@@ -70,10 +66,10 @@ fn test_update_channels(cx: &mut AppContext) {
assert_channels(
&channel_store,
&[
(0, "a".to_string(), proto::ChannelRole::Member),
(1, "y".to_string(), proto::ChannelRole::Member),
(0, "b".to_string(), proto::ChannelRole::Admin),
(1, "x".to_string(), proto::ChannelRole::Admin),
(0, "a".to_string()),
(1, "y".to_string()),
(0, "b".to_string()),
(1, "x".to_string()),
],
cx,
);
@@ -91,21 +87,18 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
id: 0,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![],
},
proto::Channel {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0],
},
proto::Channel {
id: 2,
name: "c".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Admin.into(),
parent_path: vec![0, 1],
},
],
@@ -118,9 +111,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
&channel_store,
&[
//
(0, "a".to_string(), proto::ChannelRole::Admin),
(1, "b".to_string(), proto::ChannelRole::Admin),
(2, "c".to_string(), proto::ChannelRole::Admin),
(0, "a".to_string()),
(1, "b".to_string()),
(2, "c".to_string()),
],
cx,
);
@@ -135,11 +128,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
);
// Make sure that the 1/2/3 path is gone
assert_channels(
&channel_store,
&[(0, "a".to_string(), proto::ChannelRole::Admin)],
cx,
);
assert_channels(&channel_store, &[(0, "a".to_string())], cx);
}
#[gpui::test]
@@ -156,18 +145,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
id: channel_id,
name: "the-channel".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
role: proto::ChannelRole::Member.into(),
parent_path: vec![],
}],
..Default::default()
});
cx.executor().run_until_parked();
cx.update(|cx| {
assert_channels(
&channel_store,
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
cx,
);
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
});
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
@@ -368,13 +352,13 @@ fn update_channels(
#[track_caller]
fn assert_channels(
channel_store: &Model<ChannelStore>,
expected_channels: &[(usize, String, proto::ChannelRole)],
expected_channels: &[(usize, String)],
cx: &mut AppContext,
) {
let actual = channel_store.update(cx, |store, _| {
store
.ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
.map(|(depth, channel)| (depth, channel.name.to_string()))
.collect::<Vec<_>>()
});
assert_eq!(actual, expected_channels);

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ use std::{
sync::Arc,
time::Duration,
};
use tables::*;
pub use tables::*;
use tokio::sync::{Mutex, OwnedMutexGuard};
pub use ids::*;
@@ -169,6 +169,30 @@ impl Database {
self.run(body).await
}
pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let (tx, result) = self.with_weak_transaction(&f).await?;
match result {
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
return Err(error);
}
},
Err(error) => {
tx.rollback().await?;
return Err(error);
}
}
};
self.run(body).await
}
/// The same as room_transaction, but if you need to only optionally return a Room.
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
where
@@ -284,6 +308,30 @@ impl Database {
Ok((tx, result))
}
async fn with_weak_transaction<F, Fut, T>(
&self,
f: &F,
) -> Result<(DatabaseTransaction, Result<T>)>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let tx = self
.pool
.begin_with_config(Some(IsolationLevel::ReadCommitted), None)
.await?;
let mut tx = Arc::new(Some(tx));
let result = f(TransactionHandle(tx.clone())).await;
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
return Err(anyhow!(
"couldn't complete transaction because it's still in use"
))?;
};
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> Result<T>
where
F: Future<Output = Result<T>>,
@@ -303,13 +351,14 @@ impl Database {
}
}
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
// If the error is due to a failure to serialize concurrent transactions, then retry
// this transaction after a delay. With each subsequent retry, double the delay duration.
// Also vary the delay randomly in order to ensure different database connections retry
// at different times.
if is_serialization_error(error) {
let base_delay = 4_u64 << prev_attempt_count.min(16);
const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
let base_delay = SLEEPS[prev_attempt_count];
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!(
"retrying transaction after serialization error. delay: {} ms.",
@@ -453,36 +502,6 @@ pub struct NewUserResult {
pub signup_device_id: Option<String>,
}
/// The result of moving a channel.
#[derive(Debug)]
pub struct MoveChannelResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
pub participants_to_remove: HashSet<UserId>,
pub moved_channels: HashSet<ChannelId>,
}
/// The result of renaming a channel.
#[derive(Debug)]
pub struct RenameChannelResult {
pub channel: Channel,
pub participants_to_update: HashMap<UserId, Channel>,
}
/// The result of creating a channel.
#[derive(Debug)]
pub struct CreateChannelResult {
pub channel: Channel,
pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
}
/// The result of setting a channel's visibility.
#[derive(Debug)]
pub struct SetChannelVisibilityResult {
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
pub participants_to_remove: HashSet<UserId>,
pub channels_to_remove: Vec<ChannelId>,
}
/// The result of updating a channel membership.
#[derive(Debug)]
pub struct MembershipUpdated {
@@ -522,18 +541,16 @@ pub struct Channel {
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
pub role: ChannelRole,
/// parent_path is the channel ids from the root to this one (not including this one)
pub parent_path: Vec<ChannelId>,
}
impl Channel {
fn from_model(value: channel::Model, role: ChannelRole) -> Self {
fn from_model(value: channel::Model) -> Self {
Channel {
id: value.id,
visibility: value.visibility,
name: value.clone().name,
role,
parent_path: value.ancestors().collect(),
}
}
@@ -543,7 +560,6 @@ impl Channel {
id: self.id.to_proto(),
name: self.name.clone(),
visibility: self.visibility.into(),
role: self.role.into(),
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
}
}
@@ -569,9 +585,10 @@ impl ChannelMember {
#[derive(Debug, PartialEq)]
pub struct ChannelsForUser {
pub channels: Vec<Channel>,
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
pub channel_messages: Vec<proto::UnseenChannelMessage>,
pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub latest_channel_messages: Vec<proto::ChannelMessageId>,
}
#[derive(Debug)]

View File

@@ -129,6 +129,15 @@ impl ChannelRole {
}
}
pub fn can_see_channel(&self, visibility: ChannelVisibility) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest => visibility == ChannelVisibility::Public,
Banned => false,
}
}
/// True if the role allows access to all descendant channels
pub fn can_see_all_descendants(&self) -> bool {
use ChannelRole::*;

View File

@@ -748,18 +748,11 @@ impl Database {
.await
}
pub async fn unseen_channel_buffer_changes(
pub async fn latest_channel_buffer_changes(
&self,
user_id: UserId,
channel_ids: &[ChannelId],
tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelBufferChange>> {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryIds {
ChannelId,
Id,
}
) -> Result<Vec<proto::ChannelBufferVersion>> {
let mut channel_ids_by_buffer_id = HashMap::default();
let mut rows = buffer::Entity::find()
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
@@ -771,51 +764,23 @@ impl Database {
}
drop(rows);
let mut observed_edits_by_buffer_id = HashMap::default();
let mut rows = observed_buffer_edits::Entity::find()
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
.filter(
observed_buffer_edits::Column::BufferId
.is_in(channel_ids_by_buffer_id.keys().copied()),
)
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_edits_by_buffer_id.insert(row.buffer_id, row);
}
drop(rows);
let latest_operations = self
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
.await?;
let mut changes = Vec::default();
for latest in latest_operations {
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
if (
observed.epoch,
observed.lamport_timestamp,
observed.replica_id,
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
{
continue;
}
}
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
changes.push(proto::UnseenChannelBufferChange {
channel_id: channel_id.to_proto(),
epoch: latest.epoch as u64,
Ok(latest_operations
.iter()
.flat_map(|op| {
Some(proto::ChannelBufferVersion {
channel_id: channel_ids_by_buffer_id.get(&op.buffer_id)?.to_proto(),
epoch: op.epoch as u64,
version: vec![proto::VectorClockEntry {
replica_id: latest.replica_id as u32,
timestamp: latest.lamport_timestamp as u32,
replica_id: op.replica_id as u32,
timestamp: op.lamport_timestamp as u32,
}],
});
}
}
Ok(changes)
})
})
.collect())
}
/// Returns the latest operations for the buffers with the specified IDs.

View File

@@ -1,5 +1,5 @@
use super::*;
use rpc::proto::channel_member::Kind;
use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
use sea_orm::TryGetableMany;
impl Database {
@@ -19,11 +19,7 @@ impl Database {
#[cfg(test)]
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
Ok(self
.create_channel(name, None, creator_id)
.await?
.channel
.id)
Ok(self.create_channel(name, None, creator_id).await?.0.id)
}
#[cfg(test)]
@@ -36,7 +32,7 @@ impl Database {
Ok(self
.create_channel(name, Some(parent), creator_id)
.await?
.channel
.0
.id)
}
@@ -46,10 +42,15 @@ impl Database {
name: &str,
parent_channel_id: Option<ChannelId>,
admin_id: UserId,
) -> Result<CreateChannelResult> {
) -> Result<(
Channel,
Option<channel_member::Model>,
Vec<channel_member::Model>,
)> {
let name = Self::sanitize_channel_name(name)?;
self.transaction(move |tx| async move {
let mut parent = None;
let mut membership = None;
if let Some(parent_channel_id) = parent_channel_id {
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
@@ -72,29 +73,26 @@ impl Database {
.insert(&*tx)
.await?;
let participants_to_update;
if let Some(parent) = &parent {
participants_to_update = self
.participants_to_notify_for_channel_change(parent, &*tx)
.await?;
} else {
participants_to_update = vec![];
if parent.is_none() {
membership = Some(
channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel.id),
user_id: ActiveValue::Set(admin_id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Admin),
}
.insert(&*tx)
.await?,
);
}
channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel.id),
user_id: ActiveValue::Set(admin_id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Admin),
}
.insert(&*tx)
let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(&*tx)
.await?;
};
Ok(CreateChannelResult {
channel: Channel::from_model(channel, ChannelRole::Admin),
participants_to_update,
})
Ok((Channel::from_model(channel), membership, channel_members))
})
.await
}
@@ -137,16 +135,9 @@ impl Database {
);
} else if channel.visibility == ChannelVisibility::Public {
role = Some(ChannelRole::Guest);
let channel_to_join = self
.public_ancestors_including_self(&channel, &*tx)
.await?
.first()
.cloned()
.unwrap_or(channel.clone());
channel_member::Entity::insert(channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_to_join.id),
channel_id: ActiveValue::Set(channel.root_id()),
user_id: ActiveValue::Set(user_id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Guest),
@@ -155,7 +146,7 @@ impl Database {
.await?;
accept_invite_result = Some(
self.calculate_membership_updated(&channel_to_join, user_id, &*tx)
self.calculate_membership_updated(&channel, user_id, &*tx)
.await?,
);
@@ -166,7 +157,7 @@ impl Database {
}
if role.is_none() || role == Some(ChannelRole::Banned) {
Err(anyhow!("not allowed"))?
Err(ErrorCode::Forbidden.anyhow())?
}
let role = role.unwrap();
@@ -188,76 +179,47 @@ impl Database {
channel_id: ChannelId,
visibility: ChannelVisibility,
admin_id: UserId,
) -> Result<SetChannelVisibilityResult> {
) -> Result<(Channel, Vec<channel_member::Model>)> {
self.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?;
let previous_members = self
.get_channel_participant_details_internal(&channel, &*tx)
.await?;
if visibility == ChannelVisibility::Public {
if let Some(parent_id) = channel.parent_id() {
let parent = self.get_channel_internal(parent_id, &*tx).await?;
if parent.visibility != ChannelVisibility::Public {
Err(ErrorCode::BadPublicNesting
.with_tag("direction", "parent")
.anyhow())?;
}
}
} else if visibility == ChannelVisibility::Members {
if self
.get_channel_descendants_including_self(vec![channel_id], &*tx)
.await?
.into_iter()
.any(|channel| {
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
})
{
Err(ErrorCode::BadPublicNesting
.with_tag("direction", "children")
.anyhow())?;
}
}
let mut model = channel.into_active_model();
model.visibility = ActiveValue::Set(visibility);
let channel = model.update(&*tx).await?;
let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self
.participants_to_notify_for_channel_change(&channel, &*tx)
.await?
.into_iter()
.collect();
let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(&*tx)
.await?;
let mut channels_to_remove: Vec<ChannelId> = vec![];
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
match visibility {
ChannelVisibility::Members => {
let all_descendents: Vec<ChannelId> = self
.get_channel_descendants_including_self(vec![channel_id], &*tx)
.await?
.into_iter()
.map(|channel| channel.id)
.collect();
channels_to_remove = channel::Entity::find()
.filter(
channel::Column::Id
.is_in(all_descendents)
.and(channel::Column::Visibility.eq(ChannelVisibility::Public)),
)
.all(&*tx)
.await?
.into_iter()
.map(|channel| channel.id)
.collect();
channels_to_remove.push(channel_id);
for member in previous_members {
if member.role.can_only_see_public_descendants() {
participants_to_remove.insert(member.user_id);
}
}
}
ChannelVisibility::Public => {
if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? {
let parent_updates = self
.participants_to_notify_for_channel_change(&public_parent, &*tx)
.await?;
for (user_id, channels) in parent_updates {
participants_to_update.insert(user_id, channels);
}
}
}
}
Ok(SetChannelVisibilityResult {
participants_to_update,
participants_to_remove,
channels_to_remove,
})
Ok((Channel::from_model(channel), channel_members))
})
.await
}
@@ -290,7 +252,7 @@ impl Database {
.await?;
let members_to_notify: Vec<UserId> = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.select_only()
.column(channel_member::Column::UserId)
.distinct()
@@ -327,6 +289,9 @@ impl Database {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
.await?;
if !channel.is_root() {
Err(ErrorCode::NotARootChannel.anyhow())?
}
channel_member::ActiveModel {
id: ActiveValue::NotSet,
@@ -338,7 +303,7 @@ impl Database {
.insert(&*tx)
.await?;
let channel = Channel::from_model(channel, role);
let channel = Channel::from_model(channel);
let notifications = self
.create_notification(
@@ -377,35 +342,24 @@ impl Database {
channel_id: ChannelId,
admin_id: UserId,
new_name: &str,
) -> Result<RenameChannelResult> {
) -> Result<(Channel, Vec<channel_member::Model>)> {
self.transaction(move |tx| async move {
let new_name = Self::sanitize_channel_name(new_name)?.to_string();
let channel = self.get_channel_internal(channel_id, &*tx).await?;
let role = self
.check_user_is_channel_admin(&channel, admin_id, &*tx)
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?;
let mut model = channel.into_active_model();
model.name = ActiveValue::Set(new_name.clone());
let channel = model.update(&*tx).await?;
let participants = self
.get_channel_participant_details_internal(&channel, &*tx)
let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(&*tx)
.await?;
Ok(RenameChannelResult {
channel: Channel::from_model(channel.clone(), role),
participants_to_update: participants
.iter()
.map(|participant| {
(
participant.user_id,
Channel::from_model(channel.clone(), participant.role),
)
})
.collect(),
})
Ok((Channel::from_model(channel), channel_members))
})
.await
}
@@ -580,10 +534,7 @@ impl Database {
let channels = channels
.into_iter()
.filter_map(|channel| {
let role = *role_for_channel.get(&channel.id)?;
Some(Channel::from_model(channel, role))
})
.filter_map(|channel| Some(Channel::from_model(channel)))
.collect();
Ok(channels)
@@ -591,6 +542,26 @@ impl Database {
.await
}
pub async fn get_channel_memberships(
&self,
user_id: UserId,
) -> Result<(Vec<channel_member::Model>, Vec<channel::Model>)> {
self.transaction(|tx| async move {
let memberships = channel_member::Entity::find()
.filter(channel_member::Column::UserId.eq(user_id))
.all(&*tx)
.await?;
let channels = self
.get_channel_descendants_including_self(
memberships.iter().map(|m| m.channel_id),
&*tx,
)
.await?;
Ok((memberships, channels))
})
.await
}
/// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move {
@@ -609,12 +580,16 @@ impl Database {
ancestor_channel: Option<&channel::Model>,
tx: &DatabaseTransaction,
) -> Result<ChannelsForUser> {
let mut filter = channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(true));
if let Some(ancestor) = ancestor_channel {
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
}
let channel_memberships = channel_member::Entity::find()
.filter(
channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(true)),
)
.filter(filter)
.all(&*tx)
.await?;
@@ -625,56 +600,20 @@ impl Database {
)
.await?;
let mut roles_by_channel_id: HashMap<ChannelId, ChannelRole> = HashMap::default();
for membership in channel_memberships.iter() {
roles_by_channel_id.insert(membership.channel_id, membership.role);
}
let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
let roles_by_channel_id = channel_memberships
.iter()
.map(|membership| (membership.channel_id, membership.role))
.collect::<HashMap<_, _>>();
let channels: Vec<Channel> = descendants
.into_iter()
.filter_map(|channel| {
let parent_role = channel
.parent_id()
.and_then(|parent_id| roles_by_channel_id.get(&parent_id));
let role = if let Some(parent_role) = parent_role {
let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) {
existing_role.max(*parent_role)
} else {
*parent_role
};
roles_by_channel_id.insert(channel.id, role);
role
let parent_role = roles_by_channel_id.get(&channel.root_id())?;
if parent_role.can_see_channel(channel.visibility) {
Some(Channel::from_model(channel))
} else {
*roles_by_channel_id.get(&channel.id)?
};
let can_see_parent_paths = role.can_see_all_descendants()
|| role.can_only_see_public_descendants()
&& channel.visibility == ChannelVisibility::Public;
if !can_see_parent_paths {
return None;
None
}
visible_channel_ids.insert(channel.id);
if let Some(ancestor) = ancestor_channel {
if !channel
.ancestors_including_self()
.any(|id| id == ancestor.id)
{
return None;
}
}
let mut channel = Channel::from_model(channel, role);
channel
.parent_path
.retain(|id| visible_channel_ids.contains(&id));
Some(channel)
})
.collect();
@@ -702,76 +641,21 @@ impl Database {
}
let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
let channel_buffer_changes = self
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
let latest_buffer_versions = self
.latest_channel_buffer_changes(&channel_ids, &*tx)
.await?;
let unseen_messages = self
.unseen_channel_messages(user_id, &channel_ids, &*tx)
.await?;
let latest_messages = self.latest_channel_messages(&channel_ids, &*tx).await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
channel_participants,
unseen_buffer_changes: channel_buffer_changes,
channel_messages: unseen_messages,
latest_buffer_versions,
latest_channel_messages: latest_messages,
})
}
async fn participants_to_notify_for_channel_change(
&self,
new_parent: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<(UserId, ChannelsForUser)>> {
let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
let members = self
.get_channel_participant_details_internal(new_parent, &*tx)
.await?;
for member in members.iter() {
if !member.role.can_see_all_descendants() {
continue;
}
results.push((
member.user_id,
self.get_user_channels(member.user_id, Some(new_parent), &*tx)
.await?,
))
}
let public_parents = self
.public_ancestors_including_self(new_parent, &*tx)
.await?;
let public_parent = public_parents.last();
let Some(public_parent) = public_parent else {
return Ok(results);
};
// could save some time in the common case by skipping this if the
// new channel is not public and has no public descendants.
let public_members = if public_parent == new_parent {
members
} else {
self.get_channel_participant_details_internal(public_parent, &*tx)
.await?
};
for member in public_members {
if !member.role.can_only_see_public_descendants() {
continue;
};
results.push((
member.user_id,
self.get_user_channels(member.user_id, Some(public_parent), &*tx)
.await?,
))
}
Ok(results)
}
/// Sets the role for the specified channel member.
pub async fn set_channel_member_role(
&self,
@@ -809,7 +693,7 @@ impl Database {
))
} else {
Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model(
channel, role,
channel,
)))
}
})
@@ -839,22 +723,30 @@ impl Database {
if role == ChannelRole::Admin {
Ok(members
.into_iter()
.map(|channel_member| channel_member.to_proto())
.map(|channel_member| proto::ChannelMember {
role: channel_member.role.into(),
user_id: channel_member.user_id.to_proto(),
kind: if channel_member.accepted {
Kind::Member
} else {
Kind::Invitee
}
.into(),
})
.collect())
} else {
return Ok(members
.into_iter()
.filter_map(|member| {
if member.kind == proto::channel_member::Kind::Invitee {
if !member.accepted {
return None;
}
Some(ChannelMember {
role: member.role,
user_id: member.user_id,
kind: proto::channel_member::Kind::Member,
Some(proto::ChannelMember {
role: member.role.into(),
user_id: member.user_id.to_proto(),
kind: Kind::Member.into(),
})
})
.map(|channel_member| channel_member.to_proto())
.collect());
}
}
@@ -863,83 +755,11 @@ impl Database {
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<ChannelMember>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryMemberDetails {
UserId,
Role,
IsDirectMember,
Accepted,
Visibility,
}
let mut stream = channel_member::Entity::find()
.left_join(channel::Entity)
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
.select_only()
.column(channel_member::Column::UserId)
.column(channel_member::Column::Role)
.column_as(
channel_member::Column::ChannelId.eq(channel.id),
QueryMemberDetails::IsDirectMember,
)
.column(channel_member::Column::Accepted)
.column(channel::Column::Visibility)
.into_values::<_, QueryMemberDetails>()
.stream(&*tx)
.await?;
let mut user_details: HashMap<UserId, ChannelMember> = HashMap::default();
while let Some(user_membership) = stream.next().await {
let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
UserId,
ChannelRole,
bool,
bool,
ChannelVisibility,
) = user_membership?;
let kind = match (is_direct_member, is_invite_accepted) {
(true, true) => proto::channel_member::Kind::Member,
(true, false) => proto::channel_member::Kind::Invitee,
(false, true) => proto::channel_member::Kind::AncestorMember,
(false, false) => continue,
};
if channel_role == ChannelRole::Guest
&& visibility != ChannelVisibility::Public
&& channel.visibility != ChannelVisibility::Public
{
continue;
}
if let Some(details_mut) = user_details.get_mut(&user_id) {
if channel_role.should_override(details_mut.role) {
details_mut.role = channel_role;
}
if kind == Kind::Member {
details_mut.kind = kind;
// the UI is going to be a bit confusing if you already have permissions
// that are greater than or equal to the ones you're being invited to.
} else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
details_mut.kind = kind;
}
} else {
user_details.insert(
user_id,
ChannelMember {
user_id,
kind,
role: channel_role,
},
);
}
}
Ok(user_details
.into_iter()
.map(|(_, details)| details)
.collect())
) -> Result<Vec<channel_member::Model>> {
Ok(channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(tx)
.await?)
}
/// Returns the participants in the given channel.
@@ -1018,7 +838,7 @@ impl Database {
tx: &DatabaseTransaction,
) -> Result<Option<channel_member::Model>> {
let row = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.filter(channel_member::Column::UserId.eq(user_id))
.filter(channel_member::Column::Accepted.eq(false))
.one(&*tx)
@@ -1027,33 +847,6 @@ impl Database {
Ok(row)
}
async fn public_parent_channel(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Option<channel::Model>> {
let mut path = self.public_ancestors_including_self(channel, &*tx).await?;
if path.last().unwrap().id == channel.id {
path.pop();
}
Ok(path.pop())
}
pub(crate) async fn public_ancestors_including_self(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<channel::Model>> {
let visible_channels = channel::Entity::find()
.filter(channel::Column::Id.is_in(channel.ancestors_including_self()))
.filter(channel::Column::Visibility.eq(ChannelVisibility::Public))
.order_by_asc(channel::Column::ParentPath)
.all(&*tx)
.await?;
Ok(visible_channels)
}
/// Returns the role for a user in the given channel.
pub async fn channel_role_for_user(
&self,
@@ -1061,77 +854,25 @@ impl Database {
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<Option<ChannelRole>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelMembership {
ChannelId,
Role,
Visibility,
}
let mut rows = channel_member::Entity::find()
.left_join(channel::Entity)
let membership = channel_member::Entity::find()
.filter(
channel_member::Column::ChannelId
.is_in(channel.ancestors_including_self())
.eq(channel.root_id())
.and(channel_member::Column::UserId.eq(user_id))
.and(channel_member::Column::Accepted.eq(true)),
)
.select_only()
.column(channel_member::Column::ChannelId)
.column(channel_member::Column::Role)
.column(channel::Column::Visibility)
.into_values::<_, QueryChannelMembership>()
.stream(&*tx)
.one(&*tx)
.await?;
let mut user_role: Option<ChannelRole> = None;
let Some(membership) = membership else {
return Ok(None);
};
let mut is_participant = false;
let mut current_channel_visibility = None;
// note these channels are not iterated in any particular order,
// our current logic takes the highest permission available.
while let Some(row) = rows.next().await {
let (membership_channel, role, visibility): (
ChannelId,
ChannelRole,
ChannelVisibility,
) = row?;
match role {
ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => {
if let Some(users_role) = user_role {
user_role = Some(users_role.max(role));
} else {
user_role = Some(role)
}
}
ChannelRole::Guest if visibility == ChannelVisibility::Public => {
is_participant = true
}
ChannelRole::Guest => {}
}
if channel.id == membership_channel {
current_channel_visibility = Some(visibility);
}
}
// free up database connection
drop(rows);
if is_participant && user_role.is_none() {
if current_channel_visibility.is_none() {
current_channel_visibility = channel::Entity::find()
.filter(channel::Column::Id.eq(channel.id))
.one(&*tx)
.await?
.map(|channel| channel.visibility);
}
if current_channel_visibility == Some(ChannelVisibility::Public) {
user_role = Some(ChannelRole::Guest);
}
if !membership.role.can_see_channel(channel.visibility) {
return Ok(None);
}
Ok(user_role)
Ok(Some(membership.role))
}
// Get the descendants of the given set if channels, ordered by their
@@ -1184,11 +925,10 @@ impl Database {
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
let role = self
.check_user_is_channel_participant(&channel, user_id, &*tx)
self.check_user_is_channel_participant(&channel, user_id, &*tx)
.await?;
Ok(Channel::from_model(channel, role))
Ok(Channel::from_model(channel))
})
.await
}
@@ -1201,7 +941,7 @@ impl Database {
Ok(channel::Entity::find_by_id(channel_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?)
.ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
}
pub(crate) async fn get_or_create_channel_room(
@@ -1219,7 +959,9 @@ impl Database {
let room_id = if let Some(room) = room {
if let Some(env) = room.environment {
if &env != environment {
Err(anyhow!("must join using the {} release", env))?;
Err(ErrorCode::WrongReleaseChannel
.with_tag("required", &env)
.anyhow())?;
}
}
room.id
@@ -1243,62 +985,40 @@ impl Database {
pub async fn move_channel(
&self,
channel_id: ChannelId,
new_parent_id: Option<ChannelId>,
new_parent_id: ChannelId,
admin_id: UserId,
) -> Result<Option<MoveChannelResult>> {
) -> Result<(Vec<Channel>, Vec<channel_member::Model>)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &*tx).await?;
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
.await?;
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
let new_parent_path;
let new_parent_channel;
if let Some(new_parent_id) = new_parent_id {
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
.await?;
if new_parent
.ancestors_including_self()
.any(|id| id == channel.id)
{
Err(anyhow!("cannot move a channel into one of its descendants"))?;
}
new_parent_path = new_parent.path();
new_parent_channel = Some(new_parent);
} else {
new_parent_path = String::new();
new_parent_channel = None;
};
let previous_participants = self
.get_channel_participant_details_internal(&channel, &*tx)
.await?;
let old_path = format!("{}{}/", channel.parent_path, channel.id);
let new_path = format!("{}{}/", new_parent_path, channel.id);
if old_path == new_path {
return Ok(None);
if new_parent.root_id() != channel.root_id() {
Err(anyhow!(ErrorCode::WrongMoveTarget))?;
}
if new_parent
.ancestors_including_self()
.any(|id| id == channel.id)
{
Err(anyhow!(ErrorCode::CircularNesting))?;
}
if channel.visibility == ChannelVisibility::Public
&& new_parent.visibility != ChannelVisibility::Public
{
Err(anyhow!(ErrorCode::BadPublicNesting))?;
}
let root_id = channel.root_id();
let old_path = format!("{}{}/", channel.parent_path, channel.id);
let new_path = format!("{}{}/", new_parent.path(), channel.id);
let mut model = channel.into_active_model();
model.parent_path = ActiveValue::Set(new_parent_path);
model.parent_path = ActiveValue::Set(new_parent.path());
let channel = model.update(&*tx).await?;
if new_parent_channel.is_none() {
channel_member::ActiveModel {
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
user_id: ActiveValue::Set(admin_id),
accepted: ActiveValue::Set(true),
role: ActiveValue::Set(ChannelRole::Admin),
}
.insert(&*tx)
.await?;
}
let descendent_ids =
ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values(
self.pool.get_database_backend(),
@@ -1312,35 +1032,22 @@ impl Database {
.all(&*tx)
.await?;
let participants_to_update: HashMap<_, _> = self
.participants_to_notify_for_channel_change(
new_parent_channel.as_ref().unwrap_or(&channel),
&*tx,
)
let all_moved_ids = Some(channel.id).into_iter().chain(descendent_ids);
let channels = channel::Entity::find()
.filter(channel::Column::Id.is_in(all_moved_ids))
.all(&*tx)
.await?
.into_iter()
.collect();
.map(|c| Channel::from_model(c))
.collect::<Vec<_>>();
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
for id in descendent_ids {
moved_channels.insert(id);
}
moved_channels.insert(channel_id);
let channel_members = channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(root_id))
.all(&*tx)
.await?;
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
for participant in previous_participants {
if participant.kind == proto::channel_member::Kind::AncestorMember {
if !participants_to_update.contains_key(&participant.user_id) {
participants_to_remove.insert(participant.user_id);
}
}
}
Ok(Some(MoveChannelResult {
participants_to_remove,
participants_to_update,
moved_channels,
}))
Ok((channels, channel_members))
})
.await
}

View File

@@ -385,25 +385,11 @@ impl Database {
Ok(())
}
/// Returns the unseen messages for the given user in the specified channels.
pub async fn unseen_channel_messages(
pub async fn latest_channel_messages(
&self,
user_id: UserId,
channel_ids: &[ChannelId],
tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelMessage>> {
let mut observed_messages_by_channel_id = HashMap::default();
let mut rows = observed_channel_messages::Entity::find()
.filter(observed_channel_messages::Column::UserId.eq(user_id))
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_messages_by_channel_id.insert(row.channel_id, row);
}
drop(rows);
) -> Result<Vec<proto::ChannelMessageId>> {
let mut values = String::new();
for id in channel_ids {
if !values.is_empty() {
@@ -413,7 +399,7 @@ impl Database {
}
if values.is_empty() {
return Ok(Default::default());
return Ok(Vec::default());
}
let sql = format!(
@@ -437,26 +423,20 @@ impl Database {
);
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
let last_messages = channel_message::Model::find_by_statement(stmt)
.all(&*tx)
let mut last_messages = channel_message::Model::find_by_statement(stmt)
.stream(&*tx)
.await?;
let mut changes = Vec::new();
for last_message in last_messages {
if let Some(observed_message) =
observed_messages_by_channel_id.get(&last_message.channel_id)
{
if observed_message.channel_message_id == last_message.id {
continue;
}
}
changes.push(proto::UnseenChannelMessage {
channel_id: last_message.channel_id.to_proto(),
message_id: last_message.id.to_proto(),
let mut results = Vec::new();
while let Some(result) = last_messages.next().await {
let message = result?;
results.push(proto::ChannelMessageId {
channel_id: message.channel_id.to_proto(),
message_id: message.id.to_proto(),
});
}
Ok(changes)
Ok(results)
}
/// Removes the channel message with the given ID.

View File

@@ -17,6 +17,14 @@ impl Model {
self.ancestors().last()
}
pub fn is_root(&self) -> bool {
self.parent_path.is_empty()
}
pub fn root_id(&self) -> ChannelId {
self.ancestors().next().unwrap_or(self.id)
}
pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
self.parent_path
.trim_end_matches('/')

View File

@@ -150,14 +150,13 @@ impl Drop for TestDb {
}
}
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> {
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
channels
.iter()
.map(|(id, parent_path, name, role)| Channel {
.map(|(id, parent_path, name)| Channel {
id: *id,
name: name.to_string(),
visibility: ChannelVisibility::Members,
role: *role,
parent_path: parent_path.to_vec(),
})
.collect()

View File

@@ -330,8 +330,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
db.latest_channel_buffer_changes(
&[
buffers[0].channel_id,
buffers[1].channel_id,
@@ -348,12 +347,12 @@ async fn test_channel_buffers_last_operations(db: &Database) {
pretty_assertions::assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
rpc::proto::ChannelBufferVersion {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
rpc::proto::ChannelBufferVersion {
channel_id: buffers[1].channel_id.to_proto(),
epoch: 1,
version: serialize_version(&text_buffers[1].version())
@@ -362,99 +361,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
== buffer_changes[1].version.first().unwrap().replica_id)
.collect::<Vec<_>>(),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
serialize_version(&text_buffers[1].version()).as_slice(),
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
// Observe an earlier version of the buffer.
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
&[rpc::proto::VectorClockEntry {
replica_id: 0,
timestamp: 0,
}],
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
rpc::proto::ChannelBufferVersion {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),

View File

@@ -62,23 +62,13 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!(
result.channels,
channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Admin),
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
(
livestreaming_id,
&[zed_id],
"livestreaming",
ChannelRole::Admin
),
(replace_id, &[zed_id], "replace", ChannelRole::Admin),
(rust_id, &[], "rust", ChannelRole::Admin),
(cargo_id, &[rust_id], "cargo", ChannelRole::Admin),
(
cargo_ra_id,
&[rust_id, cargo_id],
"cargo-ra",
ChannelRole::Admin
)
(zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb"),
(livestreaming_id, &[zed_id], "livestreaming",),
(replace_id, &[zed_id], "replace"),
(rust_id, &[], "rust"),
(cargo_id, &[rust_id], "cargo"),
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
],)
);
@@ -86,15 +76,10 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!(
result.channels,
channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Member),
(crdb_id, &[zed_id], "crdb", ChannelRole::Member),
(
livestreaming_id,
&[zed_id],
"livestreaming",
ChannelRole::Member
),
(replace_id, &[zed_id], "replace", ChannelRole::Member)
(zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb"),
(livestreaming_id, &[zed_id], "livestreaming",),
(replace_id, &[zed_id], "replace")
],)
);
@@ -112,15 +97,10 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!(
result.channels,
channel_tree(&[
(zed_id, &[], "zed", ChannelRole::Admin),
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
(
livestreaming_id,
&[zed_id],
"livestreaming",
ChannelRole::Admin
),
(replace_id, &[zed_id], "replace", ChannelRole::Admin)
(zed_id, &[], "zed"),
(crdb_id, &[zed_id], "crdb"),
(livestreaming_id, &[zed_id], "livestreaming",),
(replace_id, &[zed_id], "replace")
],)
);
@@ -271,14 +251,19 @@ async fn test_channel_invites(db: &Arc<Database>) {
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Admin.into(),
},
]
);
}
@@ -420,13 +405,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.await
.unwrap();
// Move to same parent should be a no-op
assert!(db
.move_channel(projects_id, Some(zed_id), user_id)
.await
.unwrap()
.is_none());
let result = db.get_channels_for_user(user_id).await.unwrap();
assert_channel_tree(
result.channels,
@@ -437,20 +415,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
],
);
// Move the project channel to the root
db.move_channel(projects_id, None, user_id).await.unwrap();
let result = db.get_channels_for_user(user_id).await.unwrap();
assert_channel_tree(
result.channels,
&[
(zed_id, &[]),
(projects_id, &[]),
(livestreaming_id, &[projects_id]),
],
);
// Can't move a channel into its ancestor
db.move_channel(projects_id, Some(livestreaming_id), user_id)
db.move_channel(projects_id, livestreaming_id, user_id)
.await
.unwrap_err();
let result = db.get_channels_for_user(user_id).await.unwrap();
@@ -458,8 +424,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
result.channels,
&[
(zed_id, &[]),
(projects_id, &[]),
(livestreaming_id, &[projects_id]),
(projects_id, &[zed_id]),
(livestreaming_id, &[zed_id, projects_id]),
],
);
}
@@ -476,32 +442,39 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
let guest = new_test_user(db, "guest@example.com").await;
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
let active_channel_id = db
let internal_channel_id = db
.create_sub_channel("active", zed_channel, admin)
.await
.unwrap();
let vim_channel_id = db
.create_sub_channel("vim", active_channel_id, admin)
let public_channel_id = db
.create_sub_channel("vim", zed_channel, admin)
.await
.unwrap();
db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin)
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member)
db.set_channel_visibility(
public_channel_id,
crate::db::ChannelVisibility::Public,
admin,
)
.await
.unwrap();
db.invite_channel_member(zed_channel, member, admin, ChannelRole::Member)
.await
.unwrap();
db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest)
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
.await
.unwrap();
db.respond_to_channel_invite(active_channel_id, member, true)
db.respond_to_channel_invite(zed_channel, member, true)
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?,
&db.get_channel_internal(public_channel_id, &*tx).await?,
admin,
&*tx,
)
@@ -511,7 +484,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?,
&db.get_channel_internal(public_channel_id, &*tx).await?,
member,
&*tx,
)
@@ -521,7 +494,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
let mut members = db
.get_channel_participant_details(vim_channel_id, admin)
.get_channel_participant_details(public_channel_id, admin)
.await
.unwrap();
@@ -532,12 +505,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[
proto::ChannelMember {
user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
@@ -548,13 +521,13 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
]
);
db.respond_to_channel_invite(vim_channel_id, guest, true)
db.respond_to_channel_invite(zed_channel, guest, true)
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await?,
&db.get_channel_internal(public_channel_id, &*tx).await?,
guest,
&*tx,
)
@@ -564,23 +537,29 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_channel_tree(channels, &[(vim_channel_id, &[])]);
assert_channel_tree(
channels,
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
);
let channels = db.get_channels_for_user(member).await.unwrap().channels;
assert_channel_tree(
channels,
&[
(active_channel_id, &[]),
(vim_channel_id, &[active_channel_id]),
(zed_channel, &[]),
(internal_channel_id, &[zed_channel]),
(public_channel_id, &[zed_channel]),
],
);
db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned)
db.set_channel_member_role(zed_channel, admin, guest, ChannelRole::Banned)
.await
.unwrap();
assert!(db
.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
&db.get_channel_internal(public_channel_id, &*tx)
.await
.unwrap(),
guest,
&*tx,
)
@@ -590,7 +569,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.is_err());
let mut members = db
.get_channel_participant_details(vim_channel_id, admin)
.get_channel_participant_details(public_channel_id, admin)
.await
.unwrap();
@@ -601,12 +580,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[
proto::ChannelMember {
user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
@@ -617,11 +596,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
]
);
db.remove_channel_member(vim_channel_id, guest, admin)
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
db.remove_channel_member(zed_channel, guest, admin)
.await
.unwrap();
@@ -631,7 +606,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
// currently people invited to parent channels are not shown here
let mut members = db
.get_channel_participant_details(vim_channel_id, admin)
.get_channel_participant_details(public_channel_id, admin)
.await
.unwrap();
@@ -642,14 +617,19 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[
proto::ChannelMember {
user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Guest.into(),
},
]
);
@@ -670,7 +650,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
assert!(db
.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(active_channel_id, &*tx)
&db.get_channel_internal(internal_channel_id, &*tx)
.await
.unwrap(),
guest,
@@ -683,7 +663,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
db.transaction(|tx| async move {
db.check_user_is_channel_participant(
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
&db.get_channel_internal(public_channel_id, &*tx)
.await
.unwrap(),
guest,
&*tx,
)
@@ -693,7 +675,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
let mut members = db
.get_channel_participant_details(vim_channel_id, admin)
.get_channel_participant_details(public_channel_id, admin)
.await
.unwrap();
@@ -704,17 +686,17 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
&[
proto::ChannelMember {
user_id: admin.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: member.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Guest.into(),
},
]
@@ -723,67 +705,10 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_channel_tree(
channels,
&[(zed_channel, &[]), (vim_channel_id, &[zed_channel])],
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
)
}
test_both_dbs!(
test_user_joins_correct_channel,
test_user_joins_correct_channel_postgres,
test_user_joins_correct_channel_sqlite
);
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
let admin = new_test_user(db, "admin@example.com").await;
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
let active_channel = db
.create_sub_channel("active", zed_channel, admin)
.await
.unwrap();
let vim_channel = db
.create_sub_channel("vim", active_channel, admin)
.await
.unwrap();
let vim2_channel = db
.create_sub_channel("vim2", vim_channel, admin)
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
let most_public = db
.transaction(|tx| async move {
Ok(db
.public_ancestors_including_self(
&db.get_channel_internal(vim_channel, &*tx).await.unwrap(),
&tx,
)
.await?
.first()
.cloned())
})
.await
.unwrap()
.unwrap()
.id;
assert_eq!(most_public, zed_channel)
}
test_both_dbs!(
test_guest_access,
test_guest_access_postgres,

View File

@@ -15,22 +15,18 @@ test_both_dbs!(
async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = new_test_user(db, "user@example.com").await;
let result = db.create_channel("channel", None, user).await.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap().0;
let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(
result.channel.id,
rpc::ConnectionId { owner_id, id: 0 },
user,
)
.await
.unwrap();
db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
.await
.unwrap();
let mut all_messages = Vec::new();
for i in 0..10 {
all_messages.push(
db.create_channel_message(
result.channel.id,
channel.id,
user,
&i.to_string(),
&[],
@@ -45,7 +41,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
}
let messages = db
.get_channel_messages(result.channel.id, user, 3, None)
.get_channel_messages(channel.id, user, 3, None)
.await
.unwrap()
.into_iter()
@@ -55,7 +51,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
let messages = db
.get_channel_messages(
result.channel.id,
channel.id,
user,
4,
Some(MessageId::from_proto(all_messages[6])),
@@ -100,7 +96,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.await
.unwrap();
// As user A, create messages that re-use the same nonces. The requests
// As user A, create messages that reuse the same nonces. The requests
// succeed, but return the same ids.
let id1 = db
.create_channel_message(
@@ -239,11 +235,10 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
.await
.unwrap();
let second_message = db
let _ = db
.create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
.await
.unwrap()
.message_id;
.unwrap();
let third_message = db
.create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
@@ -262,97 +257,27 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
.message_id;
// Check that observer has new messages
let unseen_messages = db
let latest_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
db.latest_channel_messages(&[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
latest_messages,
[
rpc::proto::UnseenChannelMessage {
rpc::proto::ChannelMessageId {
channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(),
},
rpc::proto::UnseenChannelMessage {
rpc::proto::ChannelMessageId {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
},
]
);
// Observe the second message
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer still has a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[
rpc::proto::UnseenChannelMessage {
channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(),
},
rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
},
]
);
// Observe the third message,
db.observe_channel_message(channel_1, observer, third_message)
.await
.unwrap();
// Make sure the observer does not have a new method
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
// Observe the second message again, should not regress our observed state
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer does not have a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
}
test_both_dbs!(
@@ -370,7 +295,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
.create_channel("channel", None, user_a)
.await
.unwrap()
.channel
.0
.id;
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
.await

View File

@@ -3,14 +3,12 @@ mod connection_pool;
use crate::{
auth::{self, Impersonator},
db::{
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult,
RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult,
User, UserId,
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
},
executor::Executor,
AppState, Result,
AppState, Error, Result,
};
use anyhow::anyhow;
use async_tungstenite::tungstenite::{
@@ -44,7 +42,7 @@ use rpc::{
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
};
use serde::{Serialize, Serializer};
use std::{
@@ -543,12 +541,11 @@ impl Server {
}
}
Err(error) => {
peer.respond_with_error(
receipt,
proto::Error {
message: error.to_string(),
},
)?;
let proto_err = match &error {
Error::Internal(err) => err.to_proto(),
_ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
};
peer.respond_with_error(receipt, proto_err)?;
Err(error)
}
}
@@ -604,6 +601,7 @@ impl Server {
let mut pool = this.connection_pool.lock();
pool.add_connection(connection_id, user_id, user.admin);
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
this.peer.send(connection_id, build_update_user_channels(&channels_for_user.channel_memberships))?;
this.peer.send(connection_id, build_channels_update(
channels_for_user,
channel_invites
@@ -2302,10 +2300,7 @@ async fn create_channel(
let db = session.db().await;
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let CreateChannelResult {
channel,
participants_to_update,
} = db
let (channel, owner, channel_members) = db
.create_channel(&request.name, parent_id, session.user_id)
.await?;
@@ -2315,12 +2310,29 @@ async fn create_channel(
})?;
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let update = build_channels_update(channels, vec![]);
for connection_id in connection_pool.user_connection_ids(user_id) {
if user_id == session.user_id {
continue;
}
if let Some(owner) = owner {
let update = proto::UpdateUserChannels {
channel_memberships: vec![proto::ChannelMembership {
channel_id: owner.channel_id.to_proto(),
role: owner.role.into(),
}],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(owner.user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for channel_member in channel_members {
if !channel_member.role.can_see_channel(channel.visibility) {
continue;
}
let update = proto::UpdateChannels {
channels: vec![channel.to_proto()],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
@@ -2437,7 +2449,9 @@ async fn remove_channel_member(
Ok(())
}
/// Toggle the channel between public and private
/// Toggle the channel between public and private.
/// Care is taken to maintain the invariant that public channels only descend from public channels,
/// (though members-only channels can appear at any point in the hierarchy).
async fn set_channel_visibility(
request: proto::SetChannelVisibility,
response: Response<proto::SetChannelVisibility>,
@@ -2447,27 +2461,25 @@ async fn set_channel_visibility(
let channel_id = ChannelId::from_proto(request.channel_id);
let visibility = request.visibility().into();
let SetChannelVisibilityResult {
participants_to_update,
participants_to_remove,
channels_to_remove,
} = db
let (channel, channel_members) = db
.set_channel_visibility(channel_id, visibility, session.user_id)
.await?;
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let update = build_channels_update(channels, vec![]);
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(),
..Default::default()
for member in channel_members {
let update = if member.role.can_see_channel(channel.visibility) {
proto::UpdateChannels {
channels: vec![channel.to_proto()],
..Default::default()
}
} else {
proto::UpdateChannels {
delete_channels: vec![channel.id.to_proto()],
..Default::default()
}
};
for connection_id in connection_pool.user_connection_ids(user_id) {
for connection_id in connection_pool.user_connection_ids(member.user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
@@ -2476,7 +2488,7 @@ async fn set_channel_visibility(
Ok(())
}
/// Alter the role for a user in the channel
/// Alter the role for a user in the channel.
async fn set_channel_member_role(
request: proto::SetChannelMemberRole,
response: Response<proto::SetChannelMemberRole>,
@@ -2532,10 +2544,7 @@ async fn rename_channel(
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let RenameChannelResult {
channel,
participants_to_update,
} = db
let (channel, channel_members) = db
.rename_channel(channel_id, session.user_id, &request.name)
.await?;
@@ -2544,13 +2553,15 @@ async fn rename_channel(
})?;
let connection_pool = session.connection_pool().await;
for (user_id, channel) in participants_to_update {
for connection_id in connection_pool.user_connection_ids(user_id) {
let update = proto::UpdateChannels {
channels: vec![channel.to_proto()],
..Default::default()
};
for channel_member in channel_members {
if !channel_member.role.can_see_channel(channel.visibility) {
continue;
}
let update = proto::UpdateChannels {
channels: vec![channel.to_proto()],
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
@@ -2565,49 +2576,41 @@ async fn move_channel(
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
let to = request.to.map(ChannelId::from_proto);
let to = ChannelId::from_proto(request.to);
let result = session
let (channels, channel_members) = session
.db()
.await
.move_channel(channel_id, to, session.user_id)
.await?;
notify_channel_moved(result, session).await?;
response.send(Ack {})?;
Ok(())
}
async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
let Some(MoveChannelResult {
participants_to_remove,
participants_to_update,
moved_channels,
}) = result
else {
return Ok(());
};
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
let connection_pool = session.connection_pool().await;
for (user_id, channels) in participants_to_update {
let mut update = build_channels_update(channels, vec![]);
update.delete_channels = moved_channels.clone();
for connection_id in connection_pool.user_connection_ids(user_id) {
session.peer.send(connection_id, update.clone())?;
for member in channel_members {
let channels = channels
.iter()
.filter_map(|channel| {
if member.role.can_see_channel(channel.visibility) {
Some(channel.to_proto())
} else {
None
}
})
.collect::<Vec<_>>();
if channels.is_empty() {
continue;
}
}
for user_id in participants_to_remove {
let update = proto::UpdateChannels {
delete_channels: moved_channels.clone(),
channels,
..Default::default()
};
for connection_id in connection_pool.user_connection_ids(user_id) {
for connection_id in connection_pool.user_connection_ids(member.user_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(Ack {})?;
Ok(())
}
@@ -2837,7 +2840,7 @@ async fn update_channel_buffer(
session.peer.send(
peer_id.into(),
proto::UpdateChannels {
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
channel_id: channel_id.to_proto(),
epoch: epoch as u64,
version: version.clone(),
@@ -3023,7 +3026,7 @@ async fn send_channel_message(
session.peer.send(
peer_id.into(),
proto::UpdateChannels {
unseen_channel_messages: vec![proto::UnseenChannelMessage {
latest_channel_message_ids: vec![proto::ChannelMessageId {
channel_id: channel_id.to_proto(),
message_id: message_id.to_proto(),
}],
@@ -3278,6 +3281,18 @@ fn notify_membership_updated(
user_id: UserId,
peer: &Peer,
) {
let user_channels_update = proto::UpdateUserChannels {
channel_memberships: result
.new_channels
.channel_memberships
.iter()
.map(|cm| proto::ChannelMembership {
channel_id: cm.channel_id.to_proto(),
role: cm.role.into(),
})
.collect(),
..Default::default()
};
let mut update = build_channels_update(result.new_channels, vec![]);
update.delete_channels = result
.removed_channels
@@ -3287,10 +3302,27 @@ fn notify_membership_updated(
update.remove_channel_invitations = vec![result.channel_id.to_proto()];
for connection_id in connection_pool.user_connection_ids(user_id) {
peer.send(connection_id, user_channels_update.clone())
.trace_err();
peer.send(connection_id, update.clone()).trace_err();
}
}
fn build_update_user_channels(
memberships: &Vec<db::channel_member::Model>,
) -> proto::UpdateUserChannels {
proto::UpdateUserChannels {
channel_memberships: memberships
.iter()
.map(|m| proto::ChannelMembership {
channel_id: m.channel_id.to_proto(),
role: m.role.into(),
})
.collect(),
..Default::default()
}
}
fn build_channels_update(
channels: ChannelsForUser,
channel_invites: Vec<db::Channel>,
@@ -3301,8 +3333,8 @@ fn build_channels_update(
update.channels.push(channel.to_proto());
}
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
update.unseen_channel_messages = channels.channel_messages;
update.latest_channel_buffer_versions = channels.latest_buffer_versions;
update.latest_channel_message_ids = channels.latest_channel_messages;
for (channel_id, participants) in channels.channel_participants {
update

View File

@@ -637,7 +637,6 @@ async fn test_channel_buffer_changes(
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(has_buffer_changed);
@@ -655,7 +654,6 @@ async fn test_channel_buffer_changes(
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
@@ -672,7 +670,6 @@ async fn test_channel_buffer_changes(
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
@@ -687,7 +684,6 @@ async fn test_channel_buffer_changes(
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
@@ -714,7 +710,6 @@ async fn test_channel_buffer_changes(
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(has_buffer_changed);
}

View File

@@ -195,6 +195,13 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |store, cx| {

View File

@@ -313,7 +313,6 @@ async fn test_channel_message_changes(
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
@@ -341,7 +340,6 @@ async fn test_channel_message_changes(
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(!b_has_messages);
@@ -359,7 +357,6 @@ async fn test_channel_message_changes(
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(!b_has_messages);
@@ -382,7 +379,6 @@ async fn test_channel_message_changes(
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
@@ -402,7 +398,6 @@ async fn test_channel_message_changes(
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);

View File

@@ -48,13 +48,11 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
],
);
@@ -94,7 +92,6 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Member,
}],
);
@@ -141,13 +138,11 @@ async fn test_core_channels(
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
],
@@ -169,19 +164,16 @@ async fn test_core_channels(
ExpectedChannel {
id: channel_a_id,
name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".into(),
role: ChannelRole::Member,
depth: 2,
},
],
@@ -213,19 +205,16 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".into(),
depth: 2,
role: ChannelRole::Admin,
},
],
);
@@ -247,7 +236,6 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
);
assert_channels(
@@ -257,7 +245,6 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
);
@@ -280,7 +267,6 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
);
@@ -311,7 +297,6 @@ async fn test_core_channels(
id: channel_a_id,
name: "channel-a-renamed".into(),
depth: 0,
role: ChannelRole::Admin,
}],
);
}
@@ -420,7 +405,6 @@ async fn test_channel_room(
id: zed_id,
name: "zed".into(),
depth: 0,
role: ChannelRole::Member,
}],
);
cx_b.read(|cx| {
@@ -681,7 +665,6 @@ async fn test_permissions_update_while_invited(
depth: 0,
id: rust_id,
name: "rust".into(),
role: ChannelRole::Member,
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
@@ -709,7 +692,6 @@ async fn test_permissions_update_while_invited(
depth: 0,
id: rust_id,
name: "rust".into(),
role: ChannelRole::Member,
}],
);
assert_channels(client_b.channel_store(), cx_b, &[]);
@@ -748,7 +730,6 @@ async fn test_channel_rename(
depth: 0,
id: rust_id,
name: "rust-archive".into(),
role: ChannelRole::Admin,
}],
);
@@ -760,7 +741,6 @@ async fn test_channel_rename(
depth: 0,
id: rust_id,
name: "rust-archive".into(),
role: ChannelRole::Member,
}],
);
}
@@ -889,7 +869,6 @@ async fn test_lost_channel_creation(
depth: 0,
id: channel_id,
name: "x".into(),
role: ChannelRole::Member,
}],
);
@@ -913,13 +892,11 @@ async fn test_lost_channel_creation(
depth: 0,
id: channel_id,
name: "x".into(),
role: ChannelRole::Admin,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".into(),
role: ChannelRole::Admin,
},
],
);
@@ -944,13 +921,11 @@ async fn test_lost_channel_creation(
depth: 0,
id: channel_id,
name: "x".into(),
role: ChannelRole::Member,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".into(),
role: ChannelRole::Member,
},
],
);
@@ -1035,7 +1010,7 @@ async fn test_channel_link_notifications(
let vim_channel = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("vim", None, cx)
channel_store.create_channel("vim", Some(zed_channel), cx)
})
.await
.unwrap();
@@ -1048,26 +1023,16 @@ async fn test_channel_link_notifications(
.await
.unwrap();
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(vim_channel, Some(active_channel), cx)
})
.await
.unwrap();
executor.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
client_a.channel_store(),
cx_a,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
);
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
);
assert_channels_list_shape(
client_c.channel_store(),
@@ -1078,7 +1043,7 @@ async fn test_channel_link_notifications(
let helix_channel = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.create_channel("helix", None, cx)
channel_store.create_channel("helix", Some(zed_channel), cx)
})
.await
.unwrap();
@@ -1086,7 +1051,7 @@ async fn test_channel_link_notifications(
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(helix_channel, Some(vim_channel), cx)
channel_store.move_channel(helix_channel, vim_channel, cx)
})
.await
.unwrap();
@@ -1102,6 +1067,7 @@ async fn test_channel_link_notifications(
})
.await
.unwrap();
cx_a.run_until_parked();
// the new channel shows for b and c
assert_channels_list_shape(
@@ -1110,8 +1076,8 @@ async fn test_channel_link_notifications(
&[
(zed_channel, 0),
(active_channel, 1),
(vim_channel, 2),
(helix_channel, 3),
(vim_channel, 1),
(helix_channel, 2),
],
);
assert_channels_list_shape(
@@ -1119,41 +1085,6 @@ async fn test_channel_link_notifications(
cx_c,
&[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
);
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx)
})
.await
.unwrap();
executor.run_until_parked();
// the members-only channel is still shown for c, but hidden for b
assert_channels_list_shape(
client_b.channel_store(),
cx_b,
&[
(zed_channel, 0),
(active_channel, 1),
(vim_channel, 2),
(helix_channel, 3),
],
);
cx_b.read(|cx| {
client_b.channel_store().read_with(cx, |channel_store, _| {
assert_eq!(
channel_store
.channel_for_id(vim_channel)
.unwrap()
.visibility,
proto::ChannelVisibility::Members
)
})
});
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
}
#[gpui::test]
@@ -1170,24 +1101,20 @@ async fn test_channel_membership_notifications(
let channels = server
.make_channel_tree(
&[
("zed", None),
("active", Some("zed")),
("vim", Some("active")),
],
&[("zed", None), ("vim", Some("zed")), ("opensource", None)],
(&client_a, cx_a),
)
.await;
let zed_channel = channels[0];
let _active_channel = channels[1];
let vim_channel = channels[2];
let vim_channel = channels[1];
let opensource_channel = channels[2];
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
[
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx),
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx),
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
]
}))
.await
@@ -1203,14 +1130,6 @@ async fn test_channel_membership_notifications(
.await
.unwrap();
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(vim_channel, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
// we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
@@ -1222,45 +1141,42 @@ async fn test_channel_membership_notifications(
depth: 0,
id: zed_channel,
name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
name: "vim".into(),
role: ChannelRole::Member,
},
],
);
client_a
client_b.channel_store().update(cx_b, |channel_store, _| {
channel_store.is_channel_admin(zed_channel)
});
client_b
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.remove_member(vim_channel, user_b, cx)
.update(cx_b, |channel_store, cx| {
channel_store.respond_to_channel_invite(opensource_channel, true, cx)
})
.await
.unwrap();
executor.run_until_parked();
cx_a.run_until_parked();
assert_channels(
client_b.channel_store(),
cx_b,
&[
ExpectedChannel {
depth: 0,
id: zed_channel,
name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
name: "vim".into(),
role: ChannelRole::Guest,
},
],
)
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
})
.await
.unwrap();
cx_a.run_until_parked();
client_b.channel_store().update(cx_b, |channel_store, _| {
channel_store.is_channel_admin(opensource_channel)
});
}
#[gpui::test]
@@ -1329,25 +1245,6 @@ async fn test_guest_access(
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap());
});
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx)
})
.await
.unwrap();
executor.run_until_parked();
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
.await
.unwrap();
executor.run_until_parked();
assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]);
}
#[gpui::test]
@@ -1451,7 +1348,7 @@ async fn test_channel_moving(
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.move_channel(channel_d_id, Some(channel_b_id), cx)
channel_store.move_channel(channel_d_id, channel_b_id, cx)
})
.await
.unwrap();
@@ -1476,7 +1373,6 @@ struct ExpectedChannel {
depth: usize,
id: ChannelId,
name: SharedString,
role: ChannelRole,
}
#[track_caller]
@@ -1494,7 +1390,6 @@ fn assert_channel_invitations(
depth: 0,
name: channel.name.clone(),
id: channel.id,
role: channel.role,
})
.collect::<Vec<_>>()
})
@@ -1516,7 +1411,6 @@ fn assert_channels(
depth,
name: channel.name.clone().into(),
id: channel.id,
role: channel.role,
})
.collect::<Vec<_>>()
})

View File

@@ -3884,6 +3884,7 @@ async fn test_collaborating_with_diagnostics(
// Join project as client C and observe the diagnostics.
let project_c = client_c.build_remote_project(project_id, cx_c).await;
executor.run_until_parked();
let project_c_diagnostic_summaries =
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()

View File

@@ -125,6 +125,7 @@ impl TestServer {
let channel_id = server
.make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
.await;
cx_a.run_until_parked();
(client_a, client_b, channel_id)
}

View File

@@ -183,7 +183,7 @@ impl ChannelView {
} else {
self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.notes_changed(
store.update_latest_notes_version(
channel_buffer.channel_id,
channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(),

View File

@@ -132,14 +132,6 @@ impl ChatPanel {
{
this.select_channel(channel_id, None, cx)
.detach_and_log_err(cx);
if ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
{
cx.emit(PanelEvent::Activate)
}
}
this.subscriptions.push(cx.subscribe(
@@ -274,7 +266,7 @@ impl ChatPanel {
} => {
if !self.active {
self.channel_store.update(cx, |store, cx| {
store.new_message(*channel_id, *message_id, cx)
store.update_latest_message_id(*channel_id, *message_id, cx)
})
}
}
@@ -351,9 +343,11 @@ impl ChatPanel {
this.pt_3().child(
h_flex()
.text_ui_sm()
.child(div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()).size(cx.rem_size()),
))
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
),
)
.child(
div()
.pl(cx.rem_size() + px(6.0))
@@ -665,6 +659,13 @@ impl Panel for ChatPanel {
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
fn starts_open(&self, cx: &WindowContext) -> bool {
ActiveCall::global(cx)
.read(cx)
.room()
.is_some_and(|room| room.read(cx).contains_guests())
}
}
impl EventEmitter<PanelEvent> for ChatPanel {}

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use collections::{HashMap, HashSet};
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -30,7 +30,7 @@ lazy_static! {
pub struct MessageEditor {
pub editor: View<Editor>,
channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>,
channel_members: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
@@ -108,7 +108,7 @@ impl MessageEditor {
Self {
editor,
channel_store,
users: HashMap::default(),
channel_members: HashMap::default(),
channel_id: None,
mentions: Vec::new(),
mentions_task: None,
@@ -147,8 +147,8 @@ impl MessageEditor {
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.users.clear();
self.users.extend(
self.channel_members.clear();
self.channel_members.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
@@ -221,9 +221,18 @@ impl MessageEditor {
let start_offset = end_offset - query.len();
let start_anchor = buffer.read(cx).anchor_before(start_offset);
let candidates = self
.users
.keys()
let mut names = HashSet::default();
for (github_login, _) in self.channel_members.iter() {
names.insert(github_login.clone());
}
if let Some(channel_id) = self.channel_id {
for participant in self.channel_store.read(cx).channel_participants(channel_id) {
names.insert(participant.github_login.clone());
}
}
let candidates = names
.into_iter()
.map(|user| StringMatchCandidate {
id: 0,
string: user.clone(),
@@ -283,7 +292,7 @@ impl MessageEditor {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix("@") {
if let Some(user_id) = this.users.get(username) {
if let Some(user_id) = this.channel_members.get(username) {
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);

View File

@@ -22,7 +22,10 @@ use gpui::{
};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::proto::{self, PeerId};
use rpc::{
proto::{self, ChannelVisibility, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec;
@@ -35,7 +38,7 @@ use ui::{
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{NotifyResultExt, NotifyTaskExt},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
Workspace,
};
@@ -879,7 +882,7 @@ impl CollabPanel {
.update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to join project", cx, |_, _| None);
})
.ok();
}))
@@ -1017,7 +1020,12 @@ impl CollabPanel {
)
})
})
.detach_and_notify_err(cx)
.detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
match e.error_code() {
ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
_ => None,
}
})
}),
)
} else if role == proto::ChannelRole::Member {
@@ -1038,7 +1046,7 @@ impl CollabPanel {
)
})
})
.detach_and_notify_err(cx)
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
}),
)
} else {
@@ -1126,13 +1134,6 @@ impl CollabPanel {
"Rename",
Some(Box::new(SecondaryConfirm)),
cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
)
.entry(
"Move this channel",
None,
cx.handler_for(&this, move |this, cx| {
this.start_move_channel(channel_id, cx)
}),
);
if let Some(channel_name) = clipboard_channel_name {
@@ -1145,23 +1146,52 @@ impl CollabPanel {
);
}
context_menu = context_menu
.separator()
.entry(
"Invite Members",
None,
cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
)
.entry(
if self.channel_store.read(cx).is_root_channel(channel_id) {
context_menu = context_menu.separator().entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
)
.entry(
"Delete",
} else {
context_menu = context_menu.entry(
"Move this channel",
None,
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
cx.handler_for(&this, move |this, cx| {
this.start_move_channel(channel_id, cx)
}),
);
if self.channel_store.read(cx).is_public_channel(channel_id) {
context_menu = context_menu.separator().entry(
"Make Channel Private",
None,
cx.handler_for(&this, move |this, cx| {
this.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
}),
)
} else {
context_menu = context_menu.separator().entry(
"Make Channel Public",
None,
cx.handler_for(&this, move |this, cx| {
this.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
}),
)
}
}
context_menu = context_menu.entry(
"Delete",
None,
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
);
}
context_menu
@@ -1258,7 +1288,11 @@ impl CollabPanel {
app_state,
cx,
)
.detach_and_log_err(cx);
.detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
);
}
}
ListEntry::ParticipantScreen { peer_id, .. } => {
@@ -1432,7 +1466,7 @@ impl CollabPanel {
fn leave_call(cx: &mut WindowContext) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
}
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
@@ -1478,10 +1512,6 @@ impl CollabPanel {
cx.notify();
}
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
}
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
@@ -1518,6 +1548,27 @@ impl CollabPanel {
}
}
fn set_channel_visibility(
&mut self,
channel_id: ChannelId,
visibility: ChannelVisibility,
cx: &mut ViewContext<Self>,
) {
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(channel_id, visibility, cx)
})
.detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
ErrorCode::BadPublicNesting =>
if e.error_tag("direction") == Some("parent") {
Some("To make a channel public, its parent channel must be public.".to_string())
} else {
Some("To make a channel private, all of its subchannels must be private.".to_string())
},
_ => None
});
}
fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
}
@@ -1534,14 +1585,27 @@ impl CollabPanel {
cx: &mut ViewContext<CollabPanel>,
) {
if let Some(clipboard) = self.channel_clipboard.take() {
self.channel_store.update(cx, |channel_store, cx| {
channel_store
.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
.detach_and_log_err(cx)
})
self.move_channel(clipboard.channel_id, to_channel_id, cx)
}
}
fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.move_channel(channel_id, to, cx)
})
.detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
ErrorCode::BadPublicNesting => {
Some("Public channels must have public parents".into())
}
ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
ErrorCode::WrongMoveTarget => {
Some("You cannot move a channel into a different root channel".into())
}
_ => None,
})
}
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
@@ -1610,7 +1674,12 @@ impl CollabPanel {
"Are you sure you want to remove the channel \"{}\"?",
channel.name
);
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|this, mut cx| async move {
if answer.await? == 0 {
channel_store
@@ -1631,7 +1700,12 @@ impl CollabPanel {
"Are you sure you want to remove \"{}\" from your contacts?",
github_login
);
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let answer = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Remove", "Cancel"],
);
cx.spawn(|_, mut cx| async move {
if answer.await? == 0 {
user_store
@@ -1641,7 +1715,7 @@ impl CollabPanel {
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
}
fn respond_to_contact_request(
@@ -1654,7 +1728,7 @@ impl CollabPanel {
.update(cx, |store, cx| {
store.respond_to_contact_request(user_id, accept, cx)
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
}
fn respond_to_channel_invite(
@@ -1675,7 +1749,7 @@ impl CollabPanel {
.update(cx, |call, cx| {
call.invite(recipient_user_id, Some(self.project.clone()), cx)
})
.detach_and_log_err(cx);
.detach_and_prompt_err("Call failed", cx, |_, _| None);
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
@@ -1691,7 +1765,7 @@ impl CollabPanel {
Some(handle),
cx,
)
.detach_and_log_err(cx)
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
@@ -1704,7 +1778,7 @@ impl CollabPanel {
panel.update(cx, |panel, cx| {
panel
.select_channel(channel_id, None, cx)
.detach_and_log_err(cx);
.detach_and_notify_err(cx);
});
}
});
@@ -1958,32 +2032,19 @@ impl CollabPanel {
| Section::Offline => true,
};
h_flex()
.w_full()
.group("section-header")
.child(
ListHeader::new(text)
.when(can_collapse, |header| {
header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
move |this, _, cx| {
this.toggle_section_expanded(section, cx);
},
))
})
.inset(true)
.end_slot::<AnyElement>(button)
.selected(is_selected),
)
.when(section == Section::Channels, |el| {
el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, None, cx)
})
.detach_and_log_err(cx)
}))
})
h_flex().w_full().group("section-header").child(
ListHeader::new(text)
.when(can_collapse, |header| {
header
.toggle(Some(!is_collapsed))
.on_toggle(cx.listener(move |this, _, cx| {
this.toggle_section_expanded(section, cx);
}))
})
.inset(true)
.end_slot::<AnyElement>(button)
.selected(is_selected),
)
}
fn render_contact(
@@ -2197,17 +2258,16 @@ impl CollabPanel {
Some(call_channel == channel_id)
})
.unwrap_or(false);
let is_public = self
.channel_store
.read(cx)
let channel_store = self.channel_store.read(cx);
let is_public = channel_store
.channel_for_id(channel_id)
.map(|channel| channel.visibility)
== Some(proto::ChannelVisibility::Public);
let disclosed =
has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
let has_messages_notification = channel.unseen_message_id.is_some();
let has_notes_notification = channel.unseen_note_version.is_some();
let has_messages_notification = channel_store.has_new_messages(channel_id);
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
const FACEPILE_LIMIT: usize = 3;
let participants = self.channel_store.read(cx).channel_participants(channel_id);
@@ -2238,25 +2298,36 @@ impl CollabPanel {
};
let width = self.width.unwrap_or(px(240.));
let root_id = channel.root_id();
div()
.h_6()
.id(channel_id as usize)
.group("")
.flex()
.w_full()
.on_drag(channel.clone(), move |channel, cx| {
cx.new_view(|_| DraggedChannelView {
channel: channel.clone(),
width,
.when(!channel.is_root_channel(), |el| {
el.on_drag(channel.clone(), move |channel, cx| {
cx.new_view(|_| DraggedChannelView {
channel: channel.clone(),
width,
})
})
})
.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
.drag_over::<Channel>({
move |style, dragged_channel: &Channel, cx| {
if dragged_channel.root_id() == root_id {
style.bg(cx.theme().colors().ghost_element_hover)
} else {
style
}
}
})
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
})
.detach_and_log_err(cx)
if dragged_channel.root_id() != root_id {
return;
}
this.move_channel(dragged_channel.id, channel_id, cx);
}))
.child(
ListItem::new(channel_id as usize)

View File

@@ -10,11 +10,10 @@ use gpui::{
WeakView,
};
use picker::{Picker, PickerDelegate};
use rpc::proto::channel_member;
use std::sync::Arc;
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt;
use workspace::{notifications::NotifyTaskExt, ModalView};
use workspace::{notifications::DetachAndPromptErr, ModalView};
actions!(
channel_modal,
@@ -359,10 +358,8 @@ impl PickerDelegate for ChannelModalDelegate {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_member(selected_user.id, cx);
}
Some(proto::channel_member::Kind::AncestorMember) | None => {
self.invite_member(selected_user, cx)
}
Some(proto::channel_member::Kind::Member) => {}
None => self.invite_member(selected_user, cx),
},
}
}
@@ -402,10 +399,6 @@ impl PickerDelegate for ChannelModalDelegate {
.children(
if request_status == Some(proto::channel_member::Kind::Invitee) {
Some(Label::new("Invited"))
} else if membership.map(|m| m.kind)
== Some(channel_member::Kind::AncestorMember)
{
Some(Label::new("Parent"))
} else {
None
},
@@ -498,7 +491,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
Some(())
}
@@ -530,7 +523,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
Some(())
}
@@ -556,23 +549,16 @@ impl ChannelModalDelegate {
cx.notify();
})
})
.detach_and_notify_err(cx);
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
}
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
let Some(membership) = self.member_at_index(ix) else {
return;
};
if membership.kind == proto::channel_member::Kind::AncestorMember {
return;
}
let user_id = membership.user.id;
let picker = cx.view().clone();
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
if membership.kind == channel_member::Kind::AncestorMember {
return menu.entry("Inherited membership", None, |_| {});
};
let role = membership.role;
if role == ChannelRole::Admin || role == ChannelRole::Member {

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,4 +32,4 @@ smol.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
env_logger.workspace = true
tempdir.workspace = true
tempfile.workspace = true

View File

@@ -196,7 +196,6 @@ mod tests {
use sqlez::domain::Domain;
use sqlez_macros::sql;
use tempdir::TempDir;
use crate::open_db;
@@ -220,7 +219,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
}
@@ -253,7 +255,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
{
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
@@ -297,7 +302,10 @@ mod tests {
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let tempdir = tempfile::Builder::new()
.prefix("DbTests")
.tempdir()
.unwrap();
{
// Setup the bad database
let corrupt_db =

View File

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

View File

@@ -126,6 +126,7 @@ const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
#[doc(hidden)]
@@ -369,7 +370,7 @@ pub struct Editor {
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
show_cursor_names: bool,
hovered_cursor: Option<HoveredCursor>,
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
@@ -463,6 +464,7 @@ enum SelectionHistoryMode {
Redoing,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct HoveredCursor {
replica_id: u16,
selection_id: usize,
@@ -1440,7 +1442,7 @@ impl Editor {
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
hovered_cursor: Default::default(),
hovered_cursors: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
_subscriptions: vec![
@@ -2500,34 +2502,43 @@ impl Editor {
)
});
// Comment extension on newline is allowed only for cursor selections
let comment_delimiter = language.line_comment_prefix().filter(|_| {
let comment_delimiter = language.line_comment_prefixes().filter(|_| {
let is_comment_extension_enabled =
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
is_cursor && is_comment_extension_enabled
});
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
buffer
.buffer_line_for_row(start_point.row)
.is_some_and(|(snapshot, range)| {
let mut index_of_first_non_whitespace = 0;
let line_starts_with_comment = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.take(delimiter.len())
.eq(delimiter.chars());
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + delimiter.len()
<= start_point.column as usize;
line_starts_with_comment
&& cursor_is_placed_after_comment_marker
let get_comment_delimiter = |delimiters: &[Arc<str>]| {
let max_len_of_delimiter =
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
let (snapshot, range) =
buffer.buffer_line_for_row(start_point.row)?;
let mut index_of_first_non_whitespace = 0;
let comment_candidate = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.then(|| delimiter.clone())
.take(max_len_of_delimiter)
.collect::<String>();
let comment_prefix = delimiters.iter().find(|comment_prefix| {
comment_candidate.starts_with(comment_prefix.as_ref())
})?;
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + comment_prefix.len()
<= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
Some(comment_prefix.clone())
} else {
None
}
};
let comment_delimiter = if let Some(delimiters) = comment_delimiter {
get_comment_delimiter(delimiters)
} else {
None
};
@@ -3741,7 +3752,7 @@ impl Editor {
self.show_cursor_names = true;
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(3)).await;
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
this.update(&mut cx, |this, cx| {
this.show_cursor_names = false;
cx.notify()
@@ -6111,6 +6122,7 @@ impl Editor {
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
// TODO: This is n^2, because we might check all the selections
if selections
.iter()
.find(|selection| selection.range().overlaps(&offset_range))
@@ -6220,25 +6232,76 @@ impl Editor {
pub fn select_all_matches(
&mut self,
action: &SelectAllMatches,
_action: &SelectAllMatches,
cx: &mut ViewContext<Self>,
) -> Result<()> {
self.push_to_selection_history();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
loop {
self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?;
self.select_next_match_internal(&display_map, false, None, cx)?;
let Some(select_next_state) = self.select_next_state.as_mut() else {
return Ok(());
};
if select_next_state.done {
return Ok(());
}
if self
.select_next_state
.as_ref()
.map(|selection_state| selection_state.done)
.unwrap_or(true)
let mut new_selections = self.selections.all::<usize>(cx);
let buffer = &display_map.buffer_snapshot;
let query_matches = select_next_state
.query
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()));
for query_match in query_matches {
let query_match = query_match.unwrap(); // can only fail due to I/O
let offset_range = query_match.start()..query_match.end();
let display_range = offset_range.start.to_display_point(&display_map)
..offset_range.end.to_display_point(&display_map);
if !select_next_state.wordwise
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
break;
self.selections.change_with(cx, |selections| {
new_selections.push(Selection {
id: selections.new_selection_id(),
start: offset_range.start,
end: offset_range.end,
reversed: false,
goal: SelectionGoal::None,
});
});
}
}
new_selections.sort_by_key(|selection| selection.start);
let mut ix = 0;
while ix + 1 < new_selections.len() {
let current_selection = &new_selections[ix];
let next_selection = &new_selections[ix + 1];
if current_selection.range().overlaps(&next_selection.range()) {
if current_selection.id < next_selection.id {
new_selections.remove(ix + 1);
} else {
new_selections.remove(ix);
}
} else {
ix += 1;
}
}
select_next_state.done = true;
self.unfold_ranges(
new_selections.iter().map(|selection| selection.range()),
false,
false,
cx,
);
self.change_selections(Some(Autoscroll::fit()), cx, |selections| {
selections.select(new_selections)
});
Ok(())
}
@@ -6507,7 +6570,10 @@ impl Editor {
}
// If the language has line comments, toggle those.
if let Some(full_comment_prefix) = language.line_comment_prefix() {
if let Some(full_comment_prefix) = language
.line_comment_prefixes()
.and_then(|prefixes| prefixes.first())
{
// Split the comment prefix's trailing whitespace into a separate string,
// as that portion won't be used for detecting if a line is a comment.
let comment_prefix = full_comment_prefix.trim_end_matches(' ');
@@ -6515,7 +6581,7 @@ impl Editor {
let mut all_selection_lines_are_comments = true;
for row in start_row..=end_row {
if snapshot.is_line_blank(row) && start_row < end_row {
if start_row < end_row && snapshot.is_line_blank(row) {
continue;
}

View File

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

View File

@@ -17,8 +17,8 @@ use crate::{
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
HalfPageDown, HalfPageUp, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase,
Selection, SoftWrap, ToPoint, MAX_LINE_LEN,
HalfPageDown, HalfPageUp, HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@@ -567,7 +567,7 @@ impl EditorElement {
cx,
);
hover_at(editor, Some(point), cx);
Self::update_visible_cursor(editor, point, cx);
Self::update_visible_cursor(editor, point, position_map, cx);
}
None => {
update_inlay_link_and_hover_points(
@@ -592,9 +592,10 @@ impl EditorElement {
fn update_visible_cursor(
editor: &mut Editor,
point: DisplayPoint,
position_map: &PositionMap,
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx);
let snapshot = &position_map.snapshot;
let Some(hub) = editor.collaboration_hub() else {
return;
};
@@ -612,13 +613,24 @@ impl EditorElement {
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
editor.hovered_cursor.take();
return;
};
editor.hovered_cursor.replace(crate::HoveredCursor {
let key = crate::HoveredCursor {
replica_id: selection.replica_id,
selection_id: selection.selection.id,
});
};
editor.hovered_cursors.insert(
key.clone(),
cx.spawn(|editor, mut cx| async move {
cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
editor
.update(&mut cx, |editor, cx| {
editor.hovered_cursors.remove(&key);
cx.notify();
})
.ok();
}),
);
cx.notify()
}
@@ -1986,7 +1998,9 @@ impl EditorElement {
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
let is_shown = editor.show_cursor_names || editor.hovered_cursor.as_ref().is_some_and(|c| c.replica_id == selection.replica_id && c.selection_id == selection.selection.id);
let key = HoveredCursor{replica_id: selection.replica_id, selection_id: selection.selection.id};
let is_shown = editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
remote_selections
.entry(selection.replica_id)
@@ -3568,7 +3582,7 @@ mod tests {
);
// multi-buffer support
// in DisplayPoint co-ordinates, this is what we're dealing with:
// in DisplayPoint coordinates, this is what we're dealing with:
// 0: [[file
// 1: header]]
// 2: aaaaaa

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ use gpui::AppContext;
use human_bytes::human_bytes;
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use sysinfo::{RefreshKind, System, SystemExt};
use util::channel::ReleaseChannel;
#[derive(Clone, Debug, Serialize)]
@@ -23,7 +23,7 @@ impl SystemSpecs {
.map(|v| v.to_string());
let release_channel = cx.global::<ReleaseChannel>().display_name();
let os_name = cx.app_metadata().os_name;
let system = System::new_all();
let system = System::new_with_specifics(RefreshKind::new().with_memory());
let memory = system.total_memory();
let architecture = env::consts::ARCH;
let os_version = cx

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ fsevent-sys = "3.0.2"
parking_lot.workspace = true
[dev-dependencies]
tempdir.workspace = true
tempfile.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-apple-darwin"]

View File

@@ -370,12 +370,14 @@ extern "C" {
mod tests {
use super::*;
use std::{fs, sync::mpsc, thread, time::Duration};
use tempdir::TempDir;
#[test]
fn test_event_stream_simple() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
@@ -404,7 +406,10 @@ mod tests {
#[test]
fn test_event_stream_delayed_start() {
for _ in 0..3 {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
@@ -438,7 +443,10 @@ mod tests {
#[test]
fn test_event_stream_shutdown_by_dropping_handle() {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
flush_historical_events();
@@ -465,7 +473,10 @@ mod tests {
#[test]
fn test_event_stream_shutdown_before_run() {
let dir = TempDir::new("test-event-stream").unwrap();
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));

View File

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

View File

@@ -15,7 +15,7 @@ Everything in GPUI starts with an `App`. You can create one with `App::new()`, a
## The Big Picture
GPUI offers three different registers(https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
GPUI offers three different [registers](https://en.wikipedia.org/wiki/Register_(sociolinguistics)) depending on your needs:
- State management and communication with Models. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's models. Models are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::model_context` module for more information.

View File

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

View File

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

View File

@@ -136,25 +136,6 @@ impl Render for () {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {}
}
/// A quick way to create a [`Render`]able view without having to define a new type.
#[cfg(any(test, feature = "test-support"))]
pub struct TestView(Box<dyn FnMut(&mut ViewContext<TestView>) -> AnyElement>);
#[cfg(any(test, feature = "test-support"))]
impl TestView {
/// Construct a TestView from a render closure.
pub fn new<F: FnMut(&mut ViewContext<TestView>) -> AnyElement + 'static>(f: F) -> Self {
Self(Box::new(f))
}
}
#[cfg(any(test, feature = "test-support"))]
impl Render for TestView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
(self.0)(cx)
}
}
/// You can derive [`IntoElement`] on any type that implements this trait.
/// It is used to construct reusable `components` out of plain data. Think of
/// components as a recipe for a certain pattern of elements. RenderOnce allows

View File

@@ -782,10 +782,20 @@ pub trait InteractiveElement: Sized {
}
/// Apply the given style when the given data type is dragged over this element
fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
self.interactivity()
.drag_over_styles
.push((TypeId::of::<S>(), f(StyleRefinement::default())));
fn drag_over<S: 'static>(
mut self,
f: impl 'static + Fn(StyleRefinement, &S, &WindowContext) -> StyleRefinement,
) -> Self {
self.interactivity().drag_over_styles.push((
TypeId::of::<S>(),
Box::new(move |currently_dragged: &dyn Any, cx| {
f(
StyleRefinement::default(),
currently_dragged.downcast_ref::<S>().unwrap(),
cx,
)
}),
));
self
}
@@ -1174,7 +1184,10 @@ pub struct Interactivity {
pub(crate) group_hover_style: Option<GroupStyle>,
pub(crate) active_style: Option<Box<StyleRefinement>>,
pub(crate) group_active_style: Option<GroupStyle>,
pub(crate) drag_over_styles: Vec<(TypeId, StyleRefinement)>,
pub(crate) drag_over_styles: Vec<(
TypeId,
Box<dyn Fn(&dyn Any, &mut WindowContext) -> StyleRefinement>,
)>,
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
@@ -1980,7 +1993,7 @@ impl Interactivity {
}
}
for (state_type, drag_over_style) in &self.drag_over_styles {
for (state_type, build_drag_over_style) in &self.drag_over_styles {
if *state_type == drag.value.as_ref().type_id()
&& bounds
.intersect(&cx.content_mask().bounds)
@@ -1990,7 +2003,7 @@ impl Interactivity {
cx.stacking_order(),
)
{
style.refine(drag_over_style);
style.refine(&build_drag_over_style(drag.value.as_ref(), cx));
}
}
}

View File

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

View File

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

View File

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

View File

@@ -772,7 +772,13 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().input_handler.take()
}
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
// macOs applies overrides to modal window buttons after they are added.
// Two most important for this logic are:
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
@@ -808,6 +814,9 @@ impl PlatformWindow for MacWindow {
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
if let Some(detail) = detail {
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
}
for (ix, answer) in answers
.iter()
@@ -1009,7 +1018,7 @@ fn get_scale_factor(native_window: id) -> f32 {
};
// We are not certain what triggers this, but it seems that sometimes
// this method would return 0 (https://github.com/zed-industries/community/issues/2422)
// this method would return 0 (https://github.com/zed-industries/zed/issues/6412)
// It seems most likely that this would happen if the window has no screen
// (if it is off-screen), though we'd expect to see viewDidChangeBackingProperties before
// it was rendered for real.

View File

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

View File

@@ -94,7 +94,6 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
#[derive(Debug)]
struct FocusEvent {
previous_focus_path: SmallVec<[FocusId; 8]>,
current_focus_path: SmallVec<[FocusId; 8]>,
@@ -1479,9 +1478,12 @@ impl<'a> WindowContext<'a> {
&self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.window.platform_window.prompt(level, message, answers)
self.window
.platform_window
.prompt(level, message, detail, answers)
}
/// Returns all available actions for the focused element.
@@ -2022,12 +2024,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
// Always emit a notify effect, so that handlers fire correctly
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
if !self.window.drawing {
self.window_cx.window.dirty = true;
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
}
}
@@ -2759,59 +2760,3 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
border_color: border_color.into(),
}
}
#[cfg(test)]
mod test {
use std::{cell::RefCell, rc::Rc};
use crate::{
self as gpui, div, FocusHandle, InteractiveElement, IntoElement, Render, TestAppContext,
ViewContext, VisualContext,
};
#[gpui::test]
fn test_notify_on_focus(cx: &mut TestAppContext) {
struct TestFocusView {
handle: FocusHandle,
}
impl Render for TestFocusView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().id("test").track_focus(&self.handle)
}
}
let notify_counter = Rc::new(RefCell::new(0));
let (notify_producer, cx) = cx.add_window_view(|cx| {
cx.activate_window();
let handle = cx.focus_handle();
cx.on_focus(&handle, |_, cx| {
cx.notify();
})
.detach();
TestFocusView { handle }
});
let focus_handle = cx.update(|cx| notify_producer.read(cx).handle.clone());
let _notify_consumer = cx.new_view({
|cx| {
let notify_counter = notify_counter.clone();
cx.observe(&notify_producer, move |_, _, _| {
*notify_counter.borrow_mut() += 1;
})
.detach();
}
});
cx.update(|cx| {
cx.focus(&focus_handle);
});
assert_eq!(*notify_counter.borrow(), 1);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,5 +83,5 @@ prettier = { path = "../prettier", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
git2.workspace = true
tempdir.workspace = true
tempfile.workspace = true
unindent.workspace = true

View File

@@ -590,6 +590,7 @@ impl Project {
client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_expand_project_entry);
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_on_type_formatting);
client.add_model_request_handler(Self::handle_inlay_hints);
@@ -974,8 +975,7 @@ impl Project {
// Start all the newly-enabled language servers.
for (worktree, language) in language_servers_to_start {
let worktree_path = worktree.read(cx).abs_path();
self.start_language_servers(&worktree, worktree_path, language, cx);
self.start_language_servers(&worktree, language, cx);
}
// Restart all language servers with changed initialization options.
@@ -2714,13 +2714,12 @@ impl Project {
})?;
for (adapter, server) in servers {
let workspace_config =
let settings =
cx.update(|cx| adapter.workspace_configuration(server.root_path(), cx))?;
server
.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams {
settings: workspace_config.clone(),
},
lsp::DidChangeConfigurationParams { settings },
)
.ok();
}
@@ -2774,8 +2773,8 @@ impl Project {
};
if let Some(file) = buffer_file {
let worktree = file.worktree.clone();
if let Some(tree) = worktree.read(cx).as_local() {
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
if worktree.read(cx).is_local() {
self.start_language_servers(&worktree, new_language, cx);
}
}
}
@@ -2783,7 +2782,6 @@ impl Project {
fn start_language_servers(
&mut self,
worktree: &Model<Worktree>,
worktree_path: Arc<Path>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
@@ -2793,22 +2791,14 @@ impl Project {
return;
}
let worktree_id = worktree.read(cx).id();
for adapter in language.lsp_adapters() {
self.start_language_server(
worktree_id,
worktree_path.clone(),
adapter.clone(),
language.clone(),
cx,
);
self.start_language_server(worktree, adapter.clone(), language.clone(), cx);
}
}
fn start_language_server(
&mut self,
worktree_id: WorktreeId,
worktree_path: Arc<Path>,
worktree: &Model<Worktree>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
@@ -2817,6 +2807,9 @@ impl Project {
return;
}
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
let worktree_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone());
if self.language_server_ids.contains_key(&key) {
return;
@@ -2949,20 +2942,14 @@ impl Project {
this.update(&mut cx, |this, cx| {
let worktrees = this.worktrees.clone();
for worktree in worktrees {
let worktree = match worktree.upgrade() {
Some(worktree) => worktree.read(cx),
None => continue,
};
let worktree_id = worktree.id();
let root_path = worktree.abs_path();
this.start_language_server(
worktree_id,
root_path,
adapter.clone(),
language.clone(),
cx,
);
if let Some(worktree) = worktree.upgrade() {
this.start_language_server(
&worktree,
adapter.clone(),
language.clone(),
cx,
);
}
}
})
.ok();
@@ -3176,7 +3163,7 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options().await;
let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
@@ -3332,7 +3319,7 @@ impl Project {
worktree_id: WorktreeId,
adapter_name: LanguageServerName,
cx: &mut ModelContext<Self>,
) -> Task<(Option<PathBuf>, Vec<WorktreeId>)> {
) -> Task<Vec<WorktreeId>> {
let key = (worktree_id, adapter_name);
if let Some(server_id) = self.language_server_ids.remove(&key) {
log::info!("stopping language server {}", key.1 .0);
@@ -3370,8 +3357,6 @@ impl Project {
let server_state = self.language_servers.remove(&server_id);
cx.emit(Event::LanguageServerRemoved(server_id));
cx.spawn(move |this, mut cx| async move {
let mut root_path = None;
let server = match server_state {
Some(LanguageServerState::Starting(task)) => task.await,
Some(LanguageServerState::Running { server, .. }) => Some(server),
@@ -3379,7 +3364,6 @@ impl Project {
};
if let Some(server) = server {
root_path = Some(server.root_path().clone());
if let Some(shutdown) = server.shutdown() {
shutdown.await;
}
@@ -3393,10 +3377,10 @@ impl Project {
.ok();
}
(root_path, orphaned_worktrees)
orphaned_worktrees
})
} else {
Task::ready((None, Vec::new()))
Task::ready(Vec::new())
}
}
@@ -3426,7 +3410,6 @@ impl Project {
None
}
// TODO This will break in the case where the adapter's root paths and worktrees are not equal
fn restart_language_servers(
&mut self,
worktree: Model<Worktree>,
@@ -3434,50 +3417,42 @@ impl Project {
cx: &mut ModelContext<Self>,
) {
let worktree_id = worktree.read(cx).id();
let fallback_path = worktree.read(cx).abs_path();
let mut stops = Vec::new();
for adapter in language.lsp_adapters() {
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
}
if stops.is_empty() {
let stop_tasks = language
.lsp_adapters()
.iter()
.map(|adapter| {
let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
(stop_task, adapter.name.clone())
})
.collect::<Vec<_>>();
if stop_tasks.is_empty() {
return;
}
let mut stops = stops.into_iter();
cx.spawn(move |this, mut cx| async move {
let (original_root_path, mut orphaned_worktrees) = stops.next().unwrap().await;
for stop in stops {
let (_, worktrees) = stop.await;
orphaned_worktrees.extend_from_slice(&worktrees);
// For each stopped language server, record all of the worktrees with which
// it was associated.
let mut affected_worktrees = Vec::new();
for (stop_task, language_server_name) in stop_tasks {
for affected_worktree_id in stop_task.await {
affected_worktrees.push((affected_worktree_id, language_server_name.clone()));
}
}
let this = match this.upgrade() {
Some(this) => this,
None => return,
};
this.update(&mut cx, |this, cx| {
// Attempt to restart using original server path. Fallback to passed in
// path if we could not retrieve the root path
let root_path = original_root_path
.map(|path_buf| Arc::from(path_buf.as_path()))
.unwrap_or(fallback_path);
this.start_language_servers(&worktree, root_path, language.clone(), cx);
// Restart the language server for the given worktree.
this.start_language_servers(&worktree, language.clone(), cx);
// Lookup new server ids and set them for each of the orphaned worktrees
for adapter in language.lsp_adapters() {
for (affected_worktree_id, language_server_name) in affected_worktrees {
if let Some(new_server_id) = this
.language_server_ids
.get(&(worktree_id, adapter.name.clone()))
.get(&(worktree_id, language_server_name.clone()))
.cloned()
{
for &orphaned_worktree in &orphaned_worktrees {
this.language_server_ids
.insert((orphaned_worktree, adapter.name.clone()), new_server_id);
}
this.language_server_ids
.insert((affected_worktree_id, language_server_name), new_server_id);
}
}
})
@@ -7751,6 +7726,40 @@ impl Project {
})
}
async fn handle_resolve_completion_documentation(
this: Model<Self>,
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ResolveCompletionDocumentationResponse> {
let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
let completion = this
.read_with(&mut cx, |this, _| {
let id = LanguageServerId(envelope.payload.language_server_id as usize);
let Some(server) = this.language_server_for_id(id) else {
return Err(anyhow!("No language server {id}"));
};
Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
})??
.await?;
let mut is_markdown = false;
let text = match completion.documentation {
Some(lsp::Documentation::String(text)) => text,
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
is_markdown = kind == lsp::MarkupKind::Markdown;
value
}
_ => String::new(),
};
Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
}
async fn handle_apply_code_action(
this: Model<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,

View File

@@ -60,9 +60,11 @@ impl Project {
.detach();
if let Some(python_settings) = &python_settings.as_option() {
let activate_command = Project::get_activate_command(python_settings);
let activate_script_path =
self.find_activate_script_path(python_settings, working_directory);
self.activate_python_virtual_environment(
activate_command,
activate_script_path,
&terminal_handle,
cx,
@@ -104,15 +106,24 @@ impl Project {
None
}
fn get_activate_command(settings: &VenvSettingsContent) -> &'static str {
match settings.activate_script {
terminal_settings::ActivateScript::Nushell => "overlay use",
_ => "source",
}
}
fn activate_python_virtual_environment(
&mut self,
activate_command: &'static str,
activate_script: Option<PathBuf>,
terminal_handle: &Model<Terminal>,
cx: &mut ModelContext<Project>,
) {
if let Some(activate_script) = activate_script {
// Paths are not strings so we need to jump through some hoops to format the command without `format!`
let mut command = Vec::from("source ".as_bytes());
let mut command = Vec::from(activate_command.as_bytes());
command.push(b' ');
command.extend_from_slice(activate_script.as_os_str().as_bytes());
command.push(b'\n');

View File

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

View File

@@ -58,7 +58,6 @@ pub struct ProjectPanel {
workspace: WeakView<Workspace>,
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
was_deserialized: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -244,7 +243,6 @@ impl ProjectPanel {
workspace: workspace.weak_handle(),
width: None,
pending_serialization: Task::ready(None),
was_deserialized: false,
};
this.update_visible_entries(None, cx);
@@ -324,7 +322,6 @@ impl ProjectPanel {
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.was_deserialized = true;
cx.notify();
});
}
@@ -781,6 +778,7 @@ impl ProjectPanel {
let answer = cx.prompt(
PromptLevel::Info,
&format!("Delete {file_name:?}?"),
None,
&["Delete", "Cancel"],
);
@@ -1370,7 +1368,7 @@ impl ProjectPanel {
entry_id: *entry_id,
})
})
.drag_over::<ProjectEntryId>(|style| {
.drag_over::<ProjectEntryId>(|style, _, cx| {
style.bg(cx.theme().colors().drop_target_background)
})
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
@@ -1460,9 +1458,6 @@ impl ProjectPanel {
cx.notify();
}
}
pub fn was_deserialized(&self) -> bool {
self.was_deserialized
}
}
impl Render for ProjectPanel {
@@ -1637,6 +1632,14 @@ impl Panel for ProjectPanel {
fn persistent_name() -> &'static str {
"Project Panel"
}
fn starts_open(&self, cx: &WindowContext) -> bool {
self.project.read(cx).visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()
.map_or(false, |entry| entry.is_dir())
})
}
}
impl FocusableView for ProjectPanel {
@@ -1937,7 +1940,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();
@@ -2295,7 +2297,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();
@@ -2571,7 +2572,6 @@ mod tests {
.update(cx, |workspace, cx| {
let panel = ProjectPanel::new(workspace, cx);
workspace.add_panel(panel.clone(), cx);
workspace.toggle_dock(panel.read(cx).position(cx), cx);
panel
})
.unwrap();

View File

@@ -43,6 +43,6 @@ prost-build = "0.9"
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
smol.workspace = true
tempdir.workspace = true
tempfile.workspace = true
ctor.workspace = true
env_logger.workspace = true

View File

@@ -181,7 +181,9 @@ message Envelope {
MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
SetRoomParticipantRole set_room_participant_role = 156; // Current max
SetRoomParticipantRole set_room_participant_role = 156;
UpdateUserChannels update_user_channels = 157; // current max
}
}
@@ -197,6 +199,23 @@ message Ack {}
message Error {
string message = 1;
ErrorCode code = 2;
repeated string tags = 3;
}
enum ErrorCode {
Internal = 0;
NoSuchChannel = 1;
Disconnected = 2;
SignedOut = 3;
UpgradeRequired = 4;
Forbidden = 5;
WrongReleaseChannel = 6;
NeedsCla = 7;
NotARootChannel = 8;
BadPublicNesting = 9;
CircularNesting = 10;
WrongMoveTarget = 11;
}
message Test {
@@ -979,21 +998,26 @@ message UpdateChannels {
repeated Channel channel_invitations = 5;
repeated uint64 remove_channel_invitations = 6;
repeated ChannelParticipants channel_participants = 7;
repeated UnseenChannelMessage unseen_channel_messages = 9;
repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
repeated ChannelMessageId latest_channel_message_ids = 8;
repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
}
message UnseenChannelMessage {
message UpdateUserChannels {
repeated ChannelMessageId observed_channel_message_id = 1;
repeated ChannelBufferVersion observed_channel_buffer_version = 2;
repeated ChannelMembership channel_memberships = 3;
}
message ChannelMembership {
uint64 channel_id = 1;
ChannelRole role = 2;
}
message ChannelMessageId {
uint64 channel_id = 1;
uint64 message_id = 2;
}
message UnseenChannelBufferChange {
uint64 channel_id = 1;
uint64 epoch = 2;
repeated VectorClockEntry version = 3;
}
message ChannelPermission {
uint64 channel_id = 1;
ChannelRole role = 3;
@@ -1028,7 +1052,6 @@ message ChannelMember {
enum Kind {
Member = 0;
Invitee = 1;
AncestorMember = 2;
}
}
@@ -1135,7 +1158,7 @@ message GetChannelMessagesById {
message MoveChannel {
uint64 channel_id = 1;
optional uint64 to = 2;
uint64 to = 2;
}
message JoinChannelBuffer {
@@ -1573,7 +1596,6 @@ message Channel {
uint64 id = 1;
string name = 2;
ChannelVisibility visibility = 3;
ChannelRole role = 4;
repeated uint64 parent_path = 5;
}

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

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

View File

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

View File

@@ -269,6 +269,7 @@ messages!(
(UpdateChannelBuffer, Foreground),
(UpdateChannelBufferCollaborators, Foreground),
(UpdateChannels, Foreground),
(UpdateUserChannels, Foreground),
(UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateDiffBase, Foreground),

View File

@@ -1,12 +1,14 @@
pub mod auth;
mod conn;
mod error;
mod notification;
mod peer;
pub mod proto;
pub use conn::Connection;
pub use error::*;
pub use notification::*;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 67;
pub const PROTOCOL_VERSION: u32 = 68;

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ node_runtime = { path = "../node_runtime"}
pretty_assertions.workspace = true
rand.workspace = true
unindent.workspace = true
tempdir.workspace = true
tempfile.workspace = true
ctor.workspace = true
env_logger.workspace = true

View File

@@ -63,7 +63,10 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
languages.add(rust_language);
languages.add(toml_language);
let db_dir = tempdir::TempDir::new("vector-store").unwrap();
let db_dir = tempfile::Builder::new()
.prefix("vector-store")
.tempdir()
.unwrap();
let db_path = db_dir.path().join("db.sqlite");
let embedding_provider = Arc::new(FakeEmbeddingProvider::default());

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