Compare commits

...

249 Commits

Author SHA1 Message Date
Max Brunsfeld
56634687a5 Name embedded.provisionprofile the same on stable as other channels 2023-10-25 18:02:50 +02:00
Max Brunsfeld
fd910b463d Avoid unused import in release builds 2023-10-25 17:19:47 +02:00
Joseph T. Lyons
289b001e03 v0.109.x stable 2023-10-25 16:07:12 +02:00
Kirill Bulatov
1e70dc2973 zed 0.109.1 2023-10-23 09:53:47 +02:00
Conrad Irwin
e202062152 Remove screen sharing indicator
This is now redundant given macOS has the same icon, and it panics when
you click on it :D.
2023-10-23 09:51:00 +02:00
Kirill Bulatov
ded60c675d Move prettier parsers data into languages from LSP adapters (#3150)
Release Notes:

- Fixed incorrect prettier parsers sometimes being applied to certain
files
2023-10-21 01:25:57 +02:00
Max Brunsfeld
c6466c1cce Fix possibility of infinite loop in selections_with_autoclose_regions (#3138)
Previously, that method could loop forever if the editor's autoclose
regions had unexpected selection ids.

Something must have changed recently that allowed this invariant to be
violated, but regardless, this code should not have relied on that
invariant to terminate like this.
2023-10-18 14:35:52 -04:00
Joseph T. Lyons
58dcb52336 v0.109.x preview 2023-10-18 12:30:57 -04:00
Piotr Osiewicz
99121ad5cd buffer_search: Discard empty search suggestions. (#3136)
Now when buffer_search::Deploy action is triggered (with cmd-f), we'll
keep the previous query in query_editor (if there was one) instead of
replacing it with empty query.

This addresses this bit of feedback from Jose:
> If no text is selected, `cmd + f` should not delete the text in the
search bar when refocusing

Release Notes:
- Improved buffer search by not clearing out query editor when no text
is selected and "buffer search: deploy" (default keybind: cmd-f) is
triggered.
2023-10-18 18:05:13 +02:00
Kyle Caverly
fea6d70d4d return code inside a markdown block during inline assist (#3137)
Reverted prior small change in inline prompting.
We should now only return code in a markdown block during inline
assists.
2023-10-18 10:40:53 -04:00
KCaverly
ed8a2c8793 revert change to return only the text and inside return all text inside markdown blocks 2023-10-18 10:35:11 -04:00
Joseph T. Lyons
13c7bbbac6 Shorten GitHub release message 2023-10-17 15:47:17 -04:00
Conrad Irwin
cc390ba862 Start writing role to database (#3120)
Scaffolding for guest members in channels

Release notes:
- You can now set channels to "public" which will allow anyone to join
and become a member. In a future release guests joining public channels
will have reduced permissions.
2023-10-17 13:40:58 -06:00
Conrad Irwin
04a28fe831 Fix lint errors 2023-10-17 13:32:08 -06:00
Conrad Irwin
1c5e07f4a2 update sidebar for public channels 2023-10-17 13:30:09 -06:00
Kyle Caverly
2795091f0c Introduce Context Retrieval in Inline Assistant (#3097)
This PR introduces a new Inline Assistant feature "Retrieve Context", to
dynamically fill the content in your generation prompt based on relevant
results returned from the Semantic Search for the Prompt.

Release Notes:

- Introduce "Retrieve Context" button in Inline Assistant
2023-10-17 15:04:36 -04:00
Kirill Bulatov
c380d437c6 Cap every language server logs (#3134)
* on opening a language server's logs, a new editor for server logs is
now created from `\n`-joined `VecDeque` elements instead of a buffer, as
before
* every `VecDeque` entry is a log line we receiver out of stderr or LSP
server, and their general amount is capped with `let
MAX_STORED_LOG_ENTRIES: usize = 2000;`
* currently opened editor with logs (`Editor::multi_line`) keeps getting
log lines appended and may get over this cap, but only last stored 2000
entries will be restored on reopen
* similarly, cap rpc message logs

Release Notes:

- Improved memory usage by storing less language LSP server and rpc logs
2023-10-17 21:51:21 +03:00
Kirill Bulatov
a95cce9a60 Reduce max log lines, clean log buffers better 2023-10-17 21:47:21 +03:00
Kirill Bulatov
08af830fd7 Do not create buffers for rpc logs 2023-10-17 21:43:34 +03:00
Kirill Bulatov
c872c86c4a Remove another needless log buffer 2023-10-17 21:43:34 +03:00
Kirill Bulatov
ba5c188630 Update editor with current buffer logs 2023-10-17 21:43:34 +03:00
Kirill Bulatov
5a4161d293 Do not detach subscriptions 2023-10-17 21:43:34 +03:00
Kirill Bulatov
33296802fb Add a rough prototype 2023-10-17 21:43:34 +03:00
Nate Butler
8db389313b Add link & public icons 2023-10-17 13:34:51 -04:00
Piotr Osiewicz
31241f48be workspace: Do not scan for .gitignore files if a .git directory is encountered along the way (#3135)
Partially fixes zed-industries/community#575

This PR will see one more fix to the case I've spotted while working on
this: namely, if a project has several nested repositories, e.g for a
structure:
/a
/a/.git/
/a/.gitignore
/a/b/
/a/b/.git/
/a/b/.gitignore

/b/ should not account for a's .gitignore at all - which is sort of
similar to the fix in commit #c416fbb, but for the paths in the project.

The release note is kinda bad, I'll try to reword it too.
- [ ] Improve release note.
- [x] Address the same bug for project files.

Release Notes:
- Fixed .gitignore files beyond the first .git directory being respected
by the worktree (zed-industries/community#575).
2023-10-17 18:56:03 +02:00
Conrad Irwin
5b39fc8123 Temporarily join public channels as a member 2023-10-17 10:29:43 -06:00
Conrad Irwin
3412becfc5 Fix some tests 2023-10-17 10:15:20 -06:00
Conrad Irwin
2456c077f6 Fix channel test ordering 2023-10-17 10:01:31 -06:00
Conrad Irwin
9cc55f895c Merge branch 'main' into guests 2023-10-17 09:54:17 -06:00
Conrad Irwin
851701cb6f Fix get_most_public_ancestor 2023-10-17 09:41:34 -06:00
Mikayla
465d726bd4 Minor adjustments 2023-10-17 03:05:01 -07:00
Mikayla Maki
adabf0107f Update IDs on interactive elements in LSP log viewer (#3133)
This PR fixes a panic in the LSP log viewer when rendering the popover
UI. This did not ship to preview or stable, and so does not require a
release note.

Release Notes:

- N/A
2023-10-17 02:27:37 -07:00
Mikayla Maki
fd03915f85 Adjust chat to allow channel admins to delete all messages (#3132)
As it says on the tin

Release Notes:

- Changed chat permissions so that admins of a channel can delete any
message in a channel.
2023-10-17 02:24:14 -07:00
Mikayla
a81484f13f Update IDs on interactive elements in LSP log viewer 2023-10-17 02:22:34 -07:00
Mikayla
162f625716 Adjust chat permisisons to allow deletion for channel admins 2023-10-17 02:16:17 -07:00
Conrad Irwin
b168bded1d New entitlements: (#3118)
Release Notes:

- Support Universal Links for Channel links
- Share credentials between Stable and Preview
2023-10-16 22:10:14 -06:00
Conrad Irwin
c12f0d2697 Provisioning profiles for stable and preview 2023-10-16 20:38:10 -06:00
Conrad Irwin
6ffbc3a0f5 Allow pasting ZED urls in the command palette in development 2023-10-16 20:03:44 -06:00
Conrad Irwin
2feb091961 Ensure that invitees do not have permissions
They have to accept the invite, (which joining the channel will do),
first.
2023-10-16 16:24:10 -06:00
Conrad Irwin
4e7b35c917 Make joining a channel as a guest always succeed 2023-10-16 15:14:13 -06:00
Nate Butler
247728b723 Update indexing icon
Co-Authored-By: Kyle Caverly <22121886+KCaverly@users.noreply.github.com>
2023-10-16 15:53:29 -04:00
Joseph T. Lyons
247cdb1e1a Fix telemetry-related crash on start up (#3131)
Fixes (hopefully)
[#2136](https://github.com/zed-industries/community/issues/2136).

Release Notes:

- N/A
2023-10-16 14:01:52 -04:00
Joseph T. Lyons
75fbf2ca78 Fix telemetry-related crash on start up 2023-10-16 13:07:07 -04:00
KCaverly
29f45a2e38 clean up warnings 2023-10-16 10:02:11 -04:00
KCaverly
5e1e0b4759 remove print from prompts 2023-10-16 09:55:45 -04:00
KCaverly
d2e769027a catchup with main 2023-10-16 09:47:07 -04:00
Piotr Osiewicz
cc335db9e0 editor/language: hoist out non-generic parts of edit functions. (#3130)
This reduces LLVM IR size of editor (that's one of the heaviest crates
to build) by almost 5%.

LLVM IR size of `editor` before this PR: 3280386
LLVM IR size with `editor::edit` changed: 3227092
LLVM IR size with `editor::edit` and `language::edit` changed: 3146807

Release Notes:
- N/A
2023-10-16 13:17:44 +02:00
Piotr Osiewicz
6f4008ebab copilot: Propagate action if suggest_next is not possible. (#3129)
One of our users ran into an issue where typing "true quote" characters
(option-[ for „ and option-] for ‚) was not possible; I've narrowed it
down to a collision with Copilot's NextSuggestion and PreviousSuggestion
action default keybinds. I explicitly did not want to alter the key
bindings, so I've went with a more neutral fix - one that propagates the
keystroke if there's no Copilot action to be taken (user is not using
Copilot etc). Note however that typing true quotes while using a Copilot
is still not possible, as for that we'd have to change a keybind.

Fixes zed-industries/community#2072


Release Notes:
- Fixed Copilot's "Suggest next" and "Suggest previous" actions
colliding with true quotes key bindings (`option-[` and `option-]`). The
keystrokes are now propagated if there's no Copilot action to be taken
at cursor's position.
2023-10-15 17:27:36 +02:00
Conrad Irwin
f6f9b5c8cb Wire through public access toggle 2023-10-13 16:59:30 -06:00
Conrad Irwin
f8fd77b83e fix migration 2023-10-13 15:08:09 -06:00
Conrad Irwin
af11cc6cfd show warnings by default 2023-10-13 15:07:49 -06:00
Conrad Irwin
e20bc87152 Add some sanity checks for new user channel graph 2023-10-13 14:30:20 -06:00
Conrad Irwin
bb408936e9 Ignore old admin column 2023-10-13 14:08:40 -06:00
Conrad Irwin
e050d168a7 Delete some old code, reame ChannelMembers -> Members 2023-10-13 13:39:46 -06:00
Conrad Irwin
9c6f5de551 Use new get_channel_descendants for delete 2023-10-13 13:17:19 -06:00
Conrad Irwin
a8e352a473 Rewrite get_user_channels with new permissions 2023-10-13 11:46:03 -06:00
Julia
2323fd17b0 Autocomplete docs (#3126)
Release Notes:

- Added documentation display for autocomplete items.
- Fixed autocomplete filtering blocking the Zed UI, causing hitches and
input delays with large completion lists.
- Fixed hover popup link not firing if the mouse moved a slight amount
while clicking.
- Added support for absolute path file links in hover popup and
autocomplete docs.
2023-10-13 13:26:45 -04:00
Piotr Osiewicz
bfbe4ae4b4 Piotr/z 651 vue support (#3123)
Release Notes:

- Added Vue language support.
2023-10-13 18:58:59 +02:00
Kirill Bulatov
16d9d77d88 Update diagnostics indicator when diagnostics are udpated (#3128)
Release Notes:

- Fixed diagnostics indicator not showing proper diagnostics count
2023-10-13 12:30:26 +03:00
Kirill Bulatov
803ab81eb6 Update diagnostics indicator when diagnostics are udpated 2023-10-13 12:13:18 +03:00
Kirill Bulatov
634202340b Remove zed -> ... -> semantic_index -> zed Cargo dependency cycle (#3127)
rust-analyzer complains about a bunch of dependency cycles:

```
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> assistant(Idx::<CrateData>(35)), alternative path: assistant(Idx::<CrateData>(35)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> breadcrumbs(Idx::<CrateData>(88)), alternative path: breadcrumbs(Idx::<CrateData>(88)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> collab_ui(Idx::<CrateData>(129)), alternative path: collab_ui(Idx::<CrateData>(129)) -> feedback(Idx::<CrateData>(219)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> feedback(Idx::<CrateData>(219)), alternative path: feedback(Idx::<CrateData>(219)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> quick_action_bar(Idx::<CrateData>(480)), alternative path: quick_action_bar(Idx::<CrateData>(480)) -> assistant(Idx::<CrateData>(35)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> search(Idx::<CrateData>(553)), alternative path: search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> semantic_index(Idx::<CrateData>(556)), alternative path: semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> terminal_view(Idx::<CrateData>(643)), alternative path: terminal_view(Idx::<CrateData>(643)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> vim(Idx::<CrateData>(748)), alternative path: vim(Idx::<CrateData>(748)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> welcome(Idx::<CrateData>(775)), alternative path: welcome(Idx::<CrateData>(775)) -> vim(Idx::<CrateData>(748)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> assistant(Idx::<CrateData>(35)), alternative path: assistant(Idx::<CrateData>(35)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> breadcrumbs(Idx::<CrateData>(88)), alternative path: breadcrumbs(Idx::<CrateData>(88)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> collab_ui(Idx::<CrateData>(129)), alternative path: collab_ui(Idx::<CrateData>(129)) -> feedback(Idx::<CrateData>(219)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> feedback(Idx::<CrateData>(219)), alternative path: feedback(Idx::<CrateData>(219)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> quick_action_bar(Idx::<CrateData>(480)), alternative path: quick_action_bar(Idx::<CrateData>(480)) -> assistant(Idx::<CrateData>(35)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> search(Idx::<CrateData>(553)), alternative path: search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> semantic_index(Idx::<CrateData>(556)), alternative path: semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> terminal_view(Idx::<CrateData>(643)), alternative path: terminal_view(Idx::<CrateData>(643)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> vim(Idx::<CrateData>(748)), alternative path: vim(Idx::<CrateData>(748)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
stderr: [ERROR project_model::workspace] cyclic deps: zed(Idx::<CrateData>(791)) -> welcome(Idx::<CrateData>(775)), alternative path: welcome(Idx::<CrateData>(775)) -> vim(Idx::<CrateData>(748)) -> search(Idx::<CrateData>(553)) -> semantic_index(Idx::<CrateData>(556)) -> zed(Idx::<CrateData>(791))
```

so move the example into `zed` instead.
2023-10-13 10:35:35 +03:00
Kirill Bulatov
525ff6bf74 Remove zed -> ... -> semantic_index -> zed Cargo dependency cycle 2023-10-13 10:27:08 +03:00
Conrad Irwin
65a0ebf975 Update get_channel_participant_details to include guests 2023-10-12 21:36:21 -06:00
Conrad Irwin
da2b8082b3 Rename members to participants in db crate 2023-10-12 20:42:42 -06:00
Julia
ec4391b88e Add setting to disable completion docs 2023-10-12 22:08:47 -04:00
Conrad Irwin
a7db2aa39d Add check_is_channel_participant
Refactor permission checks to load ancestor permissions into memory
for all checks to make the different logics more explicit.
2023-10-12 19:59:50 -06:00
Julia
1c3ecc4ad2 Whooooops 2023-10-12 21:00:31 -04:00
Julia
c4fc9f7ed8 Eagerly attempt to resolve missing completion documentation 2023-10-12 19:28:17 -04:00
Marshall Bowers
45f3a98359 Remove old ui and storybook crates (#3125)
This PR deletes the old `ui` and `storybook` crates in favor of their
newer variants that we'll be landing to `main` in the near future.

### Motivation

These crates are based off the old version of GPUI 2 (the `gpui2`
crate).

At this point we have since transitioned to the new version of GPUI 2
(the `gpui3` crate, currently still on the `gpui2` branch).

Having both copies around is confusing, so the old ones are going the
way of the dinosaurs.

Release Notes:

- N/A
2023-10-12 17:40:20 -04:00
Julia
d23bb3b05d Unbork markdown parse test by making links match 2023-10-12 16:18:54 -04:00
Max Brunsfeld
bac43ae38e Fix panic when following due to disconnected channel notes views (#3124)
In addition to fixing a panic, this makes it slightly more convenient to
re-open disconnected channel notes views. I didn't make it automatic,
but it will at least replace the previous, disconnected view.

Release Notes:

- Fixed a crash that sometimes occurred when following someone with a
disconnected channel notes view open.
2023-10-12 13:16:58 -07:00
Max Brunsfeld
f5d6d7caca Mark channel notes as disconnected immediately upon explicitly signing out 2023-10-12 12:39:02 -07:00
Max Brunsfeld
85fe11ff11 Replace disconnected channel notes views when re-opening the notes 2023-10-12 12:38:23 -07:00
Mikayla
78432d08ca Add channel visibility columns and protos 2023-10-12 12:21:41 -07:00
Conrad Irwin
540436a1f9 Push role refactoring through RPC/client 2023-10-12 13:05:54 -06:00
Max Brunsfeld
2e5461ee4d Exclude disconnected channel views from following messages 2023-10-12 11:55:39 -07:00
Julia
85332eacbd Race completion filter w/completion request & make not block UI 2023-10-12 13:23:26 -04:00
Julia
4688a94a54 Allow file links in markdown & filter links a bit aggressively 2023-10-12 12:11:27 -04:00
Kirill Bulatov
a50977e0fd Add prettier support (#3122) 2023-10-12 17:13:10 +03:00
Kirill Bulatov
ef73bf799c Fix license issue 2023-10-12 16:26:28 +03:00
Kirill Bulatov
7aea95704e Revert unnecessary style changes 2023-10-12 16:17:41 +03:00
Kirill Bulatov
09ef3ccf67 Fix tailwind prettier plugin discovery 2023-10-12 15:58:00 +03:00
Kirill Bulatov
12d7d8db0a Make all formatting to happen on the client's buffers, as needed 2023-10-12 15:29:57 +03:00
Kirill Bulatov
1bfde4bfa2 Add more tests 2023-10-12 15:14:51 +03:00
Kirill Bulatov
7f4ebf50d3 Make the first prettier test pass 2023-10-12 13:30:49 +03:00
Kirill Bulatov
a528c6c686 Prettier server style fixes 2023-10-12 12:31:30 +03:00
Conrad Irwin
690d9fb971 Add a role column to the database and start using it
We cannot yet stop using `admin` because stable will continue writing
it.
2023-10-11 20:05:57 -06:00
Conrad Irwin
be1800884e Make collaboration warning more useful (#3119)
Release Notes:

- Fixed the titlebar upgrade UI to restart zed when an update is
available
2023-10-11 15:35:41 -06:00
Conrad Irwin
f6d0934b5d deep considered harmful 2023-10-11 15:17:46 -06:00
Julia
a09ee3a41b Fire markdown link on mouse down
Previously any amount of mouse movement would disqualify the mouse down
and up from being a click, being a drag instead, which is a long
standing UX issue. We can get away with just firing on mouse down here
for now
2023-10-11 14:39:34 -04:00
Joseph T. Lyons
d6fa06b3be collab 0.24.0 2023-10-11 13:51:01 -04:00
Julia
0cec0c1c1d Fixup layout 2023-10-11 13:41:58 -04:00
Joseph T. Lyons
bdf1731db3 v0.109.x dev 2023-10-11 12:40:57 -04:00
Kirill Bulatov
e50f4c0ee5 Add prettier tests infrastructure 2023-10-11 19:13:28 +03:00
Conrad Irwin
2d6725a41a Make collaboration warning more useful 2023-10-11 09:50:22 -06:00
Conrad Irwin
7c867b6e54 New entitlements:
* Universal links
* Shared keychain group (to make development easier)
2023-10-11 09:36:12 -06:00
Kirill Bulatov
4a88a9e253 Initialize prettier right after the buffer gets it language 2023-10-11 14:48:32 +03:00
Kirill Bulatov
986a516bf1 Small style fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
9bf22c56cd Rebase fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b5705e079f Draft remote prettier formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2ec2036c2f Invoke remote Prettier commands 2023-10-11 12:56:29 +03:00
Kirill Bulatov
faf1d38a6d Draft local and remote prettier separation 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6c1c7eaf75 Better detect Svelte plugins 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2d5741aef8 Better prettier format logging 2023-10-11 12:56:29 +03:00
Kirill Bulatov
a9f80a603c Resolve prettier config before every formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
658b58378e Properly use WorktreeId 2023-10-11 12:56:29 +03:00
Kirill Bulatov
8a807102a6 Properly support prettier plugins 2023-10-11 12:56:29 +03:00
Kirill Bulatov
afee29ad3f Do not clear cache for default prettiers 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6ec3927dd3 Allow to configure default prettier 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b109075bf2 Watch for prettier file changes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
f4667cbc33 Resolve prettier config on server init 2023-10-11 12:56:29 +03:00
Kirill Bulatov
d021842fa1 Properly log pre-lsp prettier_server events 2023-10-11 12:56:29 +03:00
Kirill Bulatov
f42cb109a0 Improve prettier_server LSP names in the log panel 2023-10-11 12:56:29 +03:00
Kirill Bulatov
1b70e7d0df Before server startup, log to stderr 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b687270207 Implement missing prettier_server clear method 2023-10-11 12:56:29 +03:00
Kirill Bulatov
06cac18d78 Return message id in prettier_server error responses 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6cac58b34c Add prettier language servers to LSP logs panel 2023-10-11 12:56:29 +03:00
Kirill Bulatov
4b15a2bd63 Rebase fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
e8409a0108 Even more generic header printing in prettier_server 2023-10-11 12:56:29 +03:00
Kirill Bulatov
39ad3a625c Generify prettier properties, add tabWidth 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2a5b9b635b Better pass prettier options 2023-10-11 12:56:29 +03:00
Kirill Bulatov
e2056756ef Calculate the diff 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6a8e3fd02d Add more parameters into prettier invocations 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2a68f01402 Draft prettier_server formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
dca93fb177 Initialize prettier_server.js wrapper along with default prettier 2023-10-11 12:56:29 +03:00
Kirill Bulatov
010bb73ac2 Use LSP-like protocol for prettier wrapper commands 2023-10-11 12:56:29 +03:00
Kirill Bulatov
bb2cc2d157 Async-ify prettier wrapper 2023-10-11 12:56:29 +03:00
Kirill Bulatov
86618a64c6 Require prettier argument and library in the wrapper 2023-10-11 12:56:29 +03:00
Kirill Bulatov
1ff17bd15d Install default prettier and plugins on startup 2023-10-11 12:56:29 +03:00
Kirill Bulatov
12ea12e4e7 Make language adapters able to require certain bundled formatters 2023-10-11 12:56:29 +03:00
Kirill Bulatov
4f956d71e2 Slightly better prettier settings and discovery 2023-10-11 12:56:29 +03:00
Kirill Bulatov
ce6b31d938 Make NodeRuntime non-static for prettier runner 2023-10-11 12:56:29 +03:00
Kirill Bulatov
a8387b8b19 Use proper NodeRuntime in the formatter interface 2023-10-11 12:56:28 +03:00
Kirill Bulatov
a420d9cdc7 Add prettier search 2023-10-11 12:56:28 +03:00
Kirill Bulatov
a8dfa01362 Prepare prettier file lookup code infra 2023-10-11 12:56:28 +03:00
Kirill Bulatov
92f23e626e Properly connect prettier lookup/creation methods 2023-10-11 12:56:28 +03:00
Kirill Bulatov
553abd01be Draft a project part of the prettier 2023-10-11 12:56:28 +03:00
Julia
eced842dfc Get started with a prettier server package
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-10-11 12:56:28 +03:00
Joseph T. Lyons
76191fe47d Fix Discord text truncation 2023-10-11 01:54:32 -04:00
Conrad Irwin
821997d372 Revert accidental build change 2023-10-10 19:59:57 -06:00
Conrad Irwin
85b76b1143 Don't wrap on paragraphs (#3094)
Release Notes:

- vim: `{` and `}` will no longer wrap around end of file
([#2116](https://github.com/zed-industries/community/issues/2116)).
2023-10-10 19:25:40 -06:00
Conrad Irwin
9004254fbf vim: Add shift-y (#3117)
Release Notes:

- vim: Add `Y` to copy line-wise (this copies vim's behaviour, which
differs from nvim's)
2023-10-10 19:25:32 -06:00
Conrad Irwin
1de9add304 vim: Add shift-y 2023-10-10 18:46:49 -06:00
Max Brunsfeld
7a39455af9 Fix inclusion of spurious views from other projects in FollowResponse (#3116)
A logic error in https://github.com/zed-industries/zed/pull/2993 caused
follow responses to sometimes contain extra views for other unshared
projects 😱 . These views would generally fail to deserialize on the
other end. This would create a broken intermediate state, where the
following relationship was registered on the server (and on the leader's
client), but the follower didn't have the state necessary for following
into certain views.

Release Notes:

- Fixed a bug where following would sometimes fail if the leader had
another unshared project open.
2023-10-10 15:53:11 -07:00
Max Brunsfeld
96d60eff23 Fix inclusion of spurious views from other projects in FollowResponse 2023-10-10 15:40:40 -07:00
Mikayla Maki
19f774a4a4 Update channel rooms to be ephemeral (#3115)
This fixes a bug that was introduced by
https://github.com/zed-industries/zed/pull/3093, which assumed that
rooms for channels where ephemeral, by making rooms for channels
ephemeral.

Release Notes:

- N/A
2023-10-10 13:28:42 -07:00
Mikayla
d7d027bcf1 Rename release channel to enviroment 2023-10-10 13:23:03 -07:00
Joseph T. Lyons
e6228ca682 Slim down pull request template 2023-10-10 16:04:31 -04:00
Mikayla
40430cf01b Update channel rooms to be ephemeral
Remove redundant live kit initialization code
Fix bug in recent channel links changes where channel rooms would have the incorrect release set

co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
co-authored-by: Max <max@zed.dev>
2023-10-10 12:39:16 -07:00
Antonio Scandurra
0e537cced4 Revert outline summarization (#3114)
This pull request essentially reverts #3067: we noticed that only using
the function signatures produces far worse results in codegen, and so
that feels like a regression compared to before. We should re-enable
this once we have a smarter approach to fetching context during codegen,
possibly when #3097 lands.

As a drive-by, we also fixed a longstanding bug that caused codegen to
include the final line of a selection even if the selection ended at the
start of the line.

Ideally, I'd like to hot fix this to preview so that it goes to stable
during the weekly release.

/cc: @KCaverly @nathansobo 

Release Notes:

- N/A
2023-10-10 19:20:54 +02:00
Antonio Scandurra
b366592878 Don't include start of a line when selection ends at start of line 2023-10-10 19:11:13 +02:00
Antonio Scandurra
5cf92980f0 Revert summarizing file content until we can be more intelligent about what we send
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-10-10 17:51:17 +02:00
Conrad Irwin
66af1707a1 Add channel links (#3093)
Release notes:

- `mute_on_join` setting now defaults to false.
- Right click on a channel to "Copy Channel Link", these links work to
open Zed and auto-join the channel

Blocked on: https://github.com/zed-industries/zed.dev/pull/388
2023-10-10 08:53:50 -06:00
Julia
801af95a13 Make completion documentation scroll & fix accompanying panic from tag
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-10-10 10:08:29 -04:00
Julia
f5af5f7334 Avoid leaving selected item index past end of matches list
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-10-10 09:27:18 -04:00
Kirill Bulatov
1db24e5f2a Omit history files with path that does not exist on disk anymore (#3113) 2023-10-10 11:55:06 +02:00
Kirill Bulatov
639ae671ae Omit history files with path that does not exist on disk anymore 2023-10-10 12:26:48 +03:00
Julia
354882f2c0 Enable completion menu to resolve documentation when guest 2023-10-10 00:16:15 -04:00
Joseph T. Lyons
1a4e9ecfef Truncate Discord release note text (#3112)
Hopefully this works the first time 😅

Release Notes:

- N/A
2023-10-10 00:07:48 -04:00
Joseph T. Lyons
dcdd74dff4 Truncate Discord release note text 2023-10-10 00:00:57 -04:00
Conrad Irwin
d4ef764305 Merge branch 'main' into links 2023-10-09 20:08:48 -06:00
Conrad Irwin
8922437fcd code review 2023-10-09 19:06:55 -06:00
Max Brunsfeld
6e98cd5aad More small following-related fixes (#3110) 2023-10-09 15:25:22 -07:00
Max Brunsfeld
1d29709c32 Avoid possible panic in Room::most_active_project
Participants' locations might momentarily reference projects that have already been unshared.
2023-10-09 15:04:01 -07:00
Max Brunsfeld
bdcbf9b92e Add a Reconnect action, for simulating connection blips 2023-10-09 14:46:33 -07:00
Max Brunsfeld
b807b3c785 Handle participants' participant index changing
This normally doesn't happen, but it can happen if a participant
loses connection ungracefully, restarts their app, and then
explicitly joins again.
2023-10-09 14:45:19 -07:00
Max Brunsfeld
90b54a45e8 Log a warning when leader activates an unknown view 2023-10-09 14:29:45 -07:00
Kirill Bulatov
bb85d6f63e Detect file paths that end with : (#3109)
New rustc messages look like

```
thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13:
assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out
  left: 0
 right: 1
```

now and we fail to parse that `13:` bit properly, fix that.

One caveat is that we highlight the entire word including the trailing
`:`:
<img width="914" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/d653a8ff-3e6e-4e3d-b6ea-dad0c8db0f06">

this is unfortunate, but better than nothing (as now).
This is due to the fact, that we detect words with regex inside the
`terminal.rs` and send events to other place that's able to check paths
for existence (and whether that's a path at all), currently there's no
way to detect a path and sanitize it in `terminal.rs`

Release Notes:

- N/A
2023-10-09 23:16:03 +02:00
Kirill Bulatov
ba4f4e0a3e Detect file paths that end with :
New rustc messages look like

```
thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13:
assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out
  left: 0
 right: 1
```

now and we fail to parse that `13:` bit properly, fix that.
2023-10-09 23:55:58 +03:00
Max Brunsfeld
6b710dc146 Fix bug that allowed following multiple people in one pane (#3108)
I've also simplified the representation of a workspace's leaders, so
that it encodes in the type that there can only be one leader per pane.

Release Notes:

- Fixed a bug where you could accidentally follow multiple collaborators
in one pane at the same time.
2023-10-09 13:50:51 -07:00
Kirill Bulatov
0823a18cff Ignore history items' paths when matching search queries (#3107)
Follow-up of https://github.com/zed-industries/zed/pull/3059 

Before: 

![image](https://github.com/zed-industries/zed/assets/2690773/4eb2d2d1-1aa3-40b8-b782-bf2bc5f17b43)

After:

![image](https://github.com/zed-industries/zed/assets/2690773/5587d46b-9198-45fe-9372-114a95d4b7d6)

Release Notes:

- N/A
2023-10-09 22:35:11 +02:00
Max Brunsfeld
ca735ad70f Ensure there's only one leader per pane 2023-10-09 13:32:38 -07:00
Max Brunsfeld
af90077a6a Add failing test for switching leaders in a pane 2023-10-09 13:30:14 -07:00
Kirill Bulatov
9cba45910e Ignore history items' paths when matching search queries 2023-10-09 23:14:32 +03:00
Max Brunsfeld
29ccdb3cd9 Unify the two local zed scripts, take a flag for an instance count (#3106)
This PR introduces a new script for running Zed against a local collab
server, called `script/zed-local`. This script replaces the two existing
scripts that we had for this purpose: `script/zed-with-local-servers`
and `script/start-local-collaboration`.

By default, the script starts one single instance of Zed, but you can
pass a numeric flag to start 1, 2, 3 or 4 instances. So to start up two
instances side by side, (like `start-local-collaboration` script), you'd
do this:

```
script/zed-local -2
```

But you can also start *three* (or even four) instances, each taking up
a quarter of the screen, like this:

```
script/zed-local -3
```

Like before, you can pass other arguments to the script, and they will
be passed through to the first zed instance.

Also, unlike the `start-local-collaboration` script, this script now
requires a call to GitHub to determine your GitHub username. It just
logs you in as Nathan by default, unless you set `ZED_IMPERSONATE`
explicitly.
2023-10-09 12:52:20 -07:00
Max Brunsfeld
1e4f5145cf Update docs to refer to new zed-local script 2023-10-09 12:49:12 -07:00
Max Brunsfeld
a0ab9fe56b Unify the 2 local zed scripts, take a flag for instance count 2023-10-09 12:40:36 -07:00
Conrad Irwin
fb57299a1d re-trigger build with new profile? 2023-10-09 13:40:22 -06:00
Conrad Irwin
162cb19cff Only allow one release channel in a call 2023-10-09 12:59:18 -06:00
Julia
7020050b06 Fix hover_popover.rs after bad rebase 2023-10-09 14:28:53 -04:00
Conrad Irwin
abfb4490d5 Focus the currently active project if there is one
(also consider your own projects in "most_active_projects")
2023-10-09 12:05:26 -06:00
Max Brunsfeld
b2d735e573 Always log panics (#2896)
I just panicked and wanted to see the cause, but forgot that panic files
get deleted when Zed uploads them.

Release Notes:

- Panics are now written to `~/Library/Logs/Zed/Zed.log`
2023-10-09 09:21:08 -07:00
Max Brunsfeld
044701e907 Add a crate-dep-graph script, remove a few unnecessary dependencies (#3103)
This was motivated by me trying to decide which crate I should put a
`NotificationStore` in.

Run `script/crate-dep-graph` to generate an SVG showing the dependency
graph of our `crates` folder, and open it in a web browser.

After running this command, I noticed a couple of dependencies that
didn't make sense and were easy to remove.

Current dependency graph:

![Screen Shot 2023-10-06 at 1 15 42
PM](https://github.com/zed-industries/zed/assets/326587/b5008235-498a-4562-a826-cc923898c052)
2023-10-09 09:20:06 -07:00
Conrad Irwin
6084486dcd Code quality 2023-10-09 09:44:09 -06:00
Conrad Irwin
8f4d81903c Add "Copy Link" to channel right click menu 2023-10-09 09:30:00 -06:00
Conrad Irwin
5dbda70235 Fix ./script/bundle to allow passing key 2023-10-09 08:59:25 -06:00
Kirill Bulatov
38d53a6fe2 Bump curl-sys to fix Sonoma issues with it
See https://github.com/alexcrichton/curl-rust/issues/524
2023-10-09 17:09:58 +03:00
Joseph T. Lyons
77a932fe3b Add enable vim mode checkbox to welcome screen (#3105)
Had a user state that they didn't know how to enable vim mode and that
it was "almost a non-starter" for them. IMO, it is a big enough feature
to warrant being on the welcome screen.

<img width="968" alt="SCR-20231008-rnhj"
src="https://github.com/zed-industries/zed/assets/19867440/a189c646-1fa7-497c-b6d9-37cb1caa0492">

Release Notes:

- Added an `Enable vim mode` checkbox to the welcome screen
2023-10-08 21:27:31 -04:00
Joseph T. Lyons
4b2c24dd8c Add enable vim mode checkbox to welcome screen 2023-10-08 20:07:59 -04:00
Conrad Irwin
34b7537948 Add universal links support to mac platform 2023-10-06 23:15:37 -06:00
Conrad Irwin
66120fb97a Try universal link entitlement too 2023-10-06 22:25:00 -06:00
Mikayla
6de69de868 Remove change to linker args 2023-10-06 16:04:45 -07:00
Conrad Irwin
f6bc229d1d More progress and some debug logs to remove 2023-10-06 16:48:29 -06:00
Conrad Irwin
63a230f92e Make joining on boot work 2023-10-06 16:11:45 -06:00
Max Brunsfeld
f8ca86c6a7 Remove workspace -> channel dependency 2023-10-06 14:19:25 -07:00
Conrad Irwin
4128e2ffcb Fix panic if the host is not there. 2023-10-06 15:18:25 -06:00
Max Brunsfeld
3412bb75be Remove call -> channel dependency 2023-10-06 13:39:10 -07:00
Max Brunsfeld
17925ed563 Remove unnecessary dependencies on client and rpc 2023-10-06 13:14:53 -07:00
Max Brunsfeld
43da36948b Add a crate-dep-graph script for showing the crate dependency graph 2023-10-06 13:14:39 -07:00
Conrad Irwin
b58c42cd53 TEMP 2023-10-06 13:47:35 -06:00
Max Brunsfeld
9f32a6e209 collab 0.23.3 2023-10-06 11:25:46 -07:00
Max Brunsfeld
3f66caedfc Fix error in query for last N channel messages (#3100) 2023-10-06 11:24:34 -07:00
Joseph T. Lyons
1dd82df59e Use display name for release channel in panic events (#3101)
This was a mistake from long ago - something I've been meaning to fix
for a long time. All other events use `display_name()`, but panic
events, which leads to mistakes when filtering out `Zed Dev`, which
isn't the format that `dev_name()` returns. I'm adding a fix to zed.dev
as well:

- https://github.com/zed-industries/zed.dev/pull/393

so that the values are adjusted for all clients, not just ones with this
fix. I will correct the data in clickhouse, and adjust the queries in
metabase.

Release Notes:

- N/A
2023-10-06 14:20:06 -04:00
Joseph T. Lyons
81bc86be07 Use display name for release channel in panic events 2023-10-06 14:04:38 -04:00
Max Brunsfeld
663649a100 Fix error in query for last N channel messages 2023-10-06 10:58:34 -07:00
Joseph T. Lyons
1e557dddcc Add session id to panic events (#3098)
Release Notes:

- N/A
2023-10-06 13:32:45 -04:00
Julia
f18f870206 Re-enable language servers 2023-10-06 13:26:39 -04:00
Julia
9d8cff1275 If documentation included in original completion then parse up front 2023-10-06 13:26:39 -04:00
Julia
32a29cd4d3 Unbork info popover parsing/rendering and make better 2023-10-06 13:26:39 -04:00
Julia
8dca4c3f9a Don't need editor style to parse markdown 2023-10-06 13:26:39 -04:00
Julia
a881b1f5fb Wait for language to load when parsing markdown 2023-10-06 13:26:39 -04:00
Julia
ea6f366d23 If documentation exists and hasn't been parsed, do so at render and keep 2023-10-06 13:26:38 -04:00
Julia
b8876f2b17 Preparse documentation markdown when resolving completion 2023-10-06 13:26:38 -04:00
Julia
fe62423344 Asynchronously request completion documentation if not present 2023-10-06 13:26:38 -04:00
Julia
fcaf48eb49 Use completion item default data when provided 2023-10-06 13:26:38 -04:00
Julia
77ba25328c Most of getting completion documentation resolved & cached MD parsing 2023-10-06 13:26:38 -04:00
Julia
ca88717f0c Make completion docs scrollable 2023-10-06 13:26:38 -04:00
Julia
e8be14e5d6 Merge info popover's and autocomplete docs' markdown rendering 2023-10-06 13:26:38 -04:00
Julia
370a3cafd0 Add markdown rendering to alongside completion docs 2023-10-06 13:26:38 -04:00
Julia
1584dae9c2 Actually display the correct completion's doc 2023-10-06 13:26:38 -04:00
Julia
e802c072f7 Start hacking in autocomplete docs 2023-10-06 13:26:38 -04:00
Marshall Bowers
456baaa112 Mainline GPUI2 UI work (#3099)
This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Included in this is a performance improvement to make use of the
`TextLayoutCache` when calling `layout` for `Text` elements.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2023-10-06 13:18:56 -04:00
Joseph T. Lyons
2c7e37e9ff Add session id to panic events 2023-10-06 12:32:20 -04:00
Conrad Irwin
2d99b327fc Don't wrap on paragraphs
For zed-industries/community#2116
2023-10-06 10:32:15 -06:00
KCaverly
391179657c clean up redundancies in prompts and ensure tokens are being reserved for generation when filling semantic context 2023-10-06 16:43:19 +02:00
KCaverly
ecfece3ac4 catchup with main 2023-10-06 16:30:31 +02:00
KCaverly
ed548a0de2 ensure indexing is only done when permissioned 2023-10-06 16:08:36 +02:00
KCaverly
84553899f6 updated spacing for assistant context status icon 2023-10-06 15:43:28 +02:00
Piotr Osiewicz
c46137e40d chore: Upgrade to Rust 1.73 (#3096)
Release Notes:
- N/A
2023-10-06 14:50:29 +02:00
Piotr Osiewicz
b391f5615b rust: Highlight async functions in completions (#3095)
Before (code in screenshot is from this branch,
`crates/zed/languages/rust.rs:179`):

![image](https://github.com/zed-industries/zed/assets/24362066/6b709f8c-1b80-4aaa-8ddc-8db9dbca5a5e)
Notice how the last 2 entries (that are async functions) are not
highlighted properly.
After:

![image](https://github.com/zed-industries/zed/assets/24362066/88337f43-b97f-4257-9c31-54c9023e8dbb)

This is slightly suboptimal, as it's hard to tell that this is an async
function - I guess adding an `async` prefix is not really an option, as
then we should have a prefix for non-async functions too. Still, at
least you can tell that something is a function in the first place. :)

Release Notes:
- Fixed Rust async functions not being highlighted in completions.
2023-10-06 14:43:03 +02:00
KCaverly
38ccf23567 add indexing on inline assistant opening 2023-10-06 08:46:40 +03:00
KCaverly
c0a1328532 fix spawn bug from calling 2023-10-06 08:30:54 +03:00
Mikayla
31062d424f make bundle script incremental when using debug or local builds 2023-10-05 16:56:44 -07:00
Max Brunsfeld
559433bed0 Fix panic when immediately closing a window while opening paths (#3092)
Fixes this panic that I've been seeing in Slack:


[example](https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1696530575535779)


```
thread 'main' panicked at 'assertion failed: opened_items.len() == project_paths_to_open.len()'
crates/workspace/src/workspace.rs:3628
<backtrace::capture::Backtrace>::create
<backtrace::capture::Backtrace>::new
Zed::init_panic_hook::{closure#0}
std::panicking::rust_panic_with_hook
std::panicking::begin_panic_handler::{{closure}}
std::sys_common::backtrace::__rust_end_short_backtrace
_rust_begin_unwind
core::panicking::panic_fmt
core::panicking::panic
<workspace::Workspace>::new_local::{closure#0}::{closure#0}
```

I believe it was caused by a window being closed immediately, while it
was still loading some paths. There was a mismatch in expectation
between the `workspace::open_items` function (which contains this
assertion), and the `Workspace::load_workspace` method. That later
method can return an empty vector if the workspace handle is dropped
while it is executing.

Release Notes:

- Fixed a crash when closing a Zed window immediately after opening it
2023-10-05 16:28:23 -07:00
Max Brunsfeld
8fafae2cfa Fix panic when immediately closing a window while opening paths 2023-10-05 16:21:14 -07:00
Conrad Irwin
13192fa03c Code to allow opening zed:/channel/1234
Refactored a bit how url arguments are handled to avoid adding too much
extra complexity to main.
2023-10-05 14:57:45 -07:00
Conrad Irwin
b258ee5f77 Fix ./script/bundle -l 2023-10-05 14:55:39 -07:00
Conrad Irwin
a63eccf188 Add url schemes to Zed 2023-10-05 14:55:39 -07:00
KCaverly
0666fa80ac moved status to icon with additional information in tooltip 2023-10-05 16:49:25 +03:00
KCaverly
ec1b4e6f85 added initial working status in inline assistant prompt 2023-10-05 13:01:11 +03:00
KCaverly
933c21f3d3 add initial (non updating status) toast 2023-10-03 16:53:57 +03:00
KCaverly
f40d3e82c0 add user prompt for permission to index the project, for context retrieval 2023-10-03 16:26:08 +03:00
KCaverly
1a2756a232 start greedily indexing when inline assistant is started, if project has been previously indexed 2023-10-03 14:07:42 +03:00
KCaverly
ed894cc06f only render retrieve context button if semantic index is enabled 2023-10-03 12:09:35 +03:00
KCaverly
166ca2a227 catching up with main 2023-10-03 12:05:00 +03:00
KCaverly
bfe76467b0 add retrieve context button to inline assistant 2023-10-03 11:19:54 +03:00
KCaverly
e9637267ef add placeholder button for retrieving additional context 2023-10-02 19:50:57 +03:00
KCaverly
f20f096a30 searching the semantic index, and passing returned snippets to prompt generation 2023-10-02 19:15:59 +03:00
Nathan Sobo
7cd416c63e Always log panics 2023-08-25 21:42:18 -06:00
240 changed files with 8345 additions and 12891 deletions

View File

@@ -2,11 +2,4 @@
Release Notes:
- N/A
or
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
These will be removed by the person making the release.

View File

@@ -6,8 +6,8 @@ jobs:
discord_release:
runs-on: ubuntu-latest
steps:
- name: Get appropriate URL
id: get-appropriate-url
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview/latest"
@@ -15,14 +15,17 @@ jobs:
URL="https://zed.dev/releases/stable/latest"
fi
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.2.0
id: get-content
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
${{ github.event.release.body }}
maxLength: 2000
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
${{ github.event.release.body }}
content: ${{ steps.get-content.outputs.string }}

133
Cargo.lock generated
View File

@@ -103,7 +103,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"tiktoken-rs 0.5.4",
"tiktoken-rs",
"util",
]
@@ -316,12 +316,13 @@ dependencies = [
"regex",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"tiktoken-rs 0.4.5",
"tiktoken-rs",
"util",
"uuid 1.4.1",
"workspace",
@@ -1082,7 +1083,6 @@ dependencies = [
"anyhow",
"async-broadcast",
"audio",
"channel",
"client",
"collections",
"fs",
@@ -1467,7 +1467,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.23.2"
version = "0.24.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1502,6 +1502,7 @@ dependencies = [
"log",
"lsp",
"nanoid",
"node_runtime",
"parking_lot 0.11.2",
"pretty_assertions",
"project",
@@ -1624,6 +1625,7 @@ dependencies = [
"theme",
"util",
"workspace",
"zed-actions",
]
[[package]]
@@ -2079,9 +2081,9 @@ dependencies = [
[[package]]
name = "curl-sys"
version = "0.4.66+curl-8.3.0"
version = "0.4.67+curl-8.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34"
dependencies = [
"cc",
"libc",
@@ -2404,7 +2406,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
"pulldown-cmark",
"rand 0.8.5",
"rich_text",
"rpc",
@@ -2832,7 +2833,6 @@ dependencies = [
"parking_lot 0.11.2",
"regex",
"rope",
"rpc",
"serde",
"serde_derive",
"serde_json",
@@ -3990,6 +3990,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc",
@@ -5519,6 +5520,26 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettier"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"fs",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
"node_runtime",
"serde",
"serde_derive",
"serde_json",
"util",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
@@ -5631,8 +5652,10 @@ dependencies = [
"lazy_static",
"log",
"lsp",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"prettier",
"pretty_assertions",
"rand 0.8.5",
"regex",
@@ -6602,12 +6625,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustybuzz"
version = "0.3.0"
@@ -6927,7 +6944,7 @@ dependencies = [
"smol",
"tempdir",
"theme",
"tiktoken-rs 0.5.4",
"tiktoken-rs",
"tree-sitter",
"tree-sitter-cpp",
"tree-sitter-elixir",
@@ -6941,7 +6958,6 @@ dependencies = [
"unindent",
"util",
"workspace",
"zed",
]
[[package]]
@@ -7661,28 +7677,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap 4.4.4",
"fs",
"futures 0.3.28",
"gpui2",
"itertools 0.11.0",
"log",
"rust-embed",
"serde",
"settings",
"simplelog",
"strum",
"theme",
"ui",
"util",
]
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -7705,22 +7699,6 @@ name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.37",
]
[[package]]
name = "subtle"
@@ -8141,21 +8119,6 @@ dependencies = [
"weezl",
]
[[package]]
name = "tiktoken-rs"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614"
dependencies = [
"anyhow",
"base64 0.21.4",
"bstr",
"fancy-regex",
"lazy_static",
"parking_lot 0.12.1",
"rustc-hash",
]
[[package]]
name = "tiktoken-rs"
version = "0.5.4"
@@ -8816,6 +8779,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
@@ -8887,21 +8859,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ui"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"gpui2",
"rand 0.8.5",
"serde",
"settings",
"smallvec",
"strum",
"theme",
]
[[package]]
name = "unicase"
version = "2.7.0"
@@ -9665,6 +9622,7 @@ dependencies = [
"theme",
"theme_selector",
"util",
"vim",
"workspace",
]
@@ -9971,7 +9929,6 @@ dependencies = [
"async-recursion 1.0.5",
"bincode",
"call",
"channel",
"client",
"collections",
"context_menu",
@@ -9988,6 +9945,7 @@ dependencies = [
"lazy_static",
"log",
"menu",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"project",
@@ -10083,9 +10041,10 @@ dependencies = [
[[package]]
name = "zed"
version = "0.108.0"
version = "0.109.1"
dependencies = [
"activity_indicator",
"ai",
"anyhow",
"assistant",
"async-compression",
@@ -10198,6 +10157,7 @@ dependencies = [
"tree-sitter-svelte",
"tree-sitter-toml",
"tree-sitter-typescript",
"tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
"url",
@@ -10215,6 +10175,7 @@ name = "zed-actions"
version = "0.1.0"
dependencies = [
"gpui",
"serde",
]
[[package]]

View File

@@ -52,6 +52,7 @@ members = [
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
@@ -65,13 +66,11 @@ members = [
"crates/sqlez_macros",
"crates/feature_flags",
"crates/rich_text",
"crates/storybook",
"crates/sum_tree",
"crates/terminal",
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/ui",
"crates/util",
"crates/semantic_index",
"crates/vim",
@@ -150,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }

View File

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

View File

@@ -1,4 +1,4 @@
web: cd ../zed.dev && PORT=3000 npm run dev
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf

View File

@@ -83,9 +83,7 @@ foreman start
If you want to run Zed pointed at the local servers, you can run:
```
script/zed-with-local-servers
# or...
script/zed-with-local-servers --release
script/zed-local
```
### Dump element JSON

3
assets/icons/link.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.51192 3.00541C9.18827 2.54594 10.0434 2.53694 10.6788 2.95419C10.823 3.04893 10.9771 3.1993 11.389 3.61119C11.8009 4.02307 11.9513 4.17714 12.046 4.32141C12.4632 4.95675 12.4542 5.81192 11.9948 6.48827C11.8899 6.64264 11.7276 6.80811 11.3006 7.23511L10.6819 7.85383C10.4866 8.04909 10.4866 8.36567 10.6819 8.56093C10.8772 8.7562 11.1937 8.7562 11.389 8.56093L12.0077 7.94221L12.0507 7.89929C12.4203 7.52976 12.6568 7.2933 12.822 7.0502C13.4972 6.05623 13.5321 4.76252 12.8819 3.77248C12.7233 3.53102 12.4922 3.30001 12.1408 2.94871L12.0961 2.90408L12.0515 2.85942C11.7002 2.508 11.4692 2.27689 11.2277 2.11832C10.2377 1.46813 8.94396 1.50299 7.94999 2.17822C7.70689 2.34336 7.47042 2.57991 7.10088 2.94955L7.05797 2.99247L6.43926 3.61119C6.24399 3.80645 6.24399 4.12303 6.43926 4.31829C6.63452 4.51355 6.9511 4.51355 7.14636 4.31829L7.76508 3.69957C8.19208 3.27257 8.35755 3.11027 8.51192 3.00541ZM4.31794 7.14672C4.5132 6.95146 4.5132 6.63487 4.31794 6.43961C4.12267 6.24435 3.80609 6.24435 3.61083 6.43961L2.99211 7.05833L2.9492 7.10124C2.57955 7.47077 2.34301 7.70724 2.17786 7.95035C1.50263 8.94432 1.46778 10.238 2.11797 11.2281C2.27654 11.4695 2.50764 11.7005 2.85908 12.0518L2.90372 12.0965L2.94835 12.1411C3.29965 12.4925 3.53066 12.7237 3.77212 12.8822C4.76217 13.5324 6.05587 13.4976 7.04984 12.8223C7.29294 12.6572 7.52941 12.4206 7.89894 12.051L7.89895 12.051L7.94186 12.0081L8.56058 11.3894C8.75584 11.1941 8.75584 10.8775 8.56058 10.6823C8.36531 10.487 8.04873 10.487 7.85347 10.6823L7.23475 11.301C6.80775 11.728 6.64228 11.8903 6.48792 11.9951C5.81156 12.4546 4.9564 12.4636 4.32105 12.0464C4.17679 11.9516 4.02272 11.8012 3.61083 11.3894C3.19894 10.9775 3.04858 10.8234 2.95383 10.6791C2.53659 10.0438 2.54558 9.18863 3.00505 8.51227C3.10991 8.35791 3.27222 8.19244 3.69922 7.76544L4.31794 7.14672ZM9.6217 6.08558C9.81696 5.89032 9.81696 5.57373 9.6217 5.37847C9.42644 5.18321 9.10986 5.18321 8.91459 5.37847L5.37906 8.91401C5.1838 9.10927 5.1838 9.42585 5.37906 9.62111C5.57432 9.81637 5.8909 9.81637 6.08617 9.62111L9.6217 6.08558Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

3
assets/icons/public.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74393 2.00204C3.41963 1.97524 3.13502 2.37572 3.10823 2.70001C3.08143 3.0243 3.32558 3.47321 3.64986 3.50001C7.99878 3.85934 11.1406 7.00122 11.5 11.3501C11.5267 11.6744 11.9756 12.0269 12.3 12C12.6243 11.9733 13.0247 11.5804 12.998 11.2561C12.5912 6.33295 8.66704 2.40882 3.74393 2.00204ZM2.9 6.00001C2.96411 5.68099 3.33084 5.29361 3.64986 5.35772C6.66377 5.96341 9.03654 8.33618 9.64223 11.3501C9.70634 11.6691 9.319 12.0359 8.99999 12.1C8.68097 12.1641 8.06411 11.819 7.99999 11.5C7.48788 8.95167 6.0483 7.51213 3.49999 7.00001C3.18097 6.9359 2.8359 6.31902 2.9 6.00001ZM2 9.20001C2.0641 8.88099 2.38635 8.65788 2.70537 8.722C4.50255 9.08317 5.91684 10.4975 6.27801 12.2946C6.34212 12.6137 6.13547 12.9242 5.81646 12.9883C5.49744 13.0525 4.86411 12.819 4.8 12.5C4.53239 11.1683 3.83158 10.4676 2.5 10.2C2.18098 10.1359 1.93588 9.51902 2 9.20001Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

8
assets/icons/update.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.90321 7.29677C1.90321 10.341 4.11041 12.4147 6.58893 12.8439C6.87255 12.893 7.06266 13.1627 7.01355 13.4464C6.96444 13.73 6.69471 13.9201 6.41109 13.871C3.49942 13.3668 0.86084 10.9127 0.86084 7.29677C0.860839 5.76009 1.55996 4.55245 2.37639 3.63377C2.96124 2.97568 3.63034 2.44135 4.16846 2.03202L2.53205 2.03202C2.25591 2.03202 2.03205 1.80816 2.03205 1.53202C2.03205 1.25588 2.25591 1.03202 2.53205 1.03202L5.53205 1.03202C5.80819 1.03202 6.03205 1.25588 6.03205 1.53202L6.03205 4.53202C6.03205 4.80816 5.80819 5.03202 5.53205 5.03202C5.25591 5.03202 5.03205 4.80816 5.03205 4.53202L5.03205 2.68645L5.03054 2.68759L5.03045 2.68766L5.03044 2.68767L5.03043 2.68767C4.45896 3.11868 3.76059 3.64538 3.15554 4.3262C2.44102 5.13021 1.90321 6.10154 1.90321 7.29677ZM13.0109 7.70321C13.0109 4.69115 10.8505 2.6296 8.40384 2.17029C8.12093 2.11718 7.93465 1.84479 7.98776 1.56188C8.04087 1.27898 8.31326 1.0927 8.59616 1.14581C11.4704 1.68541 14.0532 4.12605 14.0532 7.70321C14.0532 9.23988 13.3541 10.4475 12.5377 11.3662C11.9528 12.0243 11.2837 12.5586 10.7456 12.968L12.3821 12.968C12.6582 12.968 12.8821 13.1918 12.8821 13.468C12.8821 13.7441 12.6582 13.968 12.3821 13.968L9.38205 13.968C9.10591 13.968 8.88205 13.7441 8.88205 13.468L8.88205 10.468C8.88205 10.1918 9.10591 9.96796 9.38205 9.96796C9.65819 9.96796 9.88205 10.1918 9.88205 10.468L9.88205 12.3135L9.88362 12.3123C10.4551 11.8813 11.1535 11.3546 11.7585 10.6738C12.4731 9.86976 13.0109 8.89844 13.0109 7.70321Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -408,6 +408,7 @@
"vim::PushOperator",
"Yank"
],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",

View File

@@ -50,6 +50,9 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// Whether to show wrap guides in the editor. Setting this to true will
// show a guide at the 'preferred_line_length' value if softwrap is set to
// 'preferred_line_length', and will show any additional guides as specified
@@ -76,7 +79,7 @@
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone muted by default
"mute_on_join": true
"mute_on_join": false
},
// Scrollbar related settings
"scrollbar": {
@@ -199,7 +202,12 @@
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// 3. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
@@ -429,6 +437,16 @@
"tab_size": 2
}
},
// Zed's Prettier integration settings.
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
// project has no other Prettier installed.
"prettier": {
// Use regular Prettier json configuration:
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
// "singleQuote": true
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.

View File

@@ -85,25 +85,6 @@ impl Embedding {
}
}
// impl FromSql for Embedding {
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
// let bytes = value.as_blob()?;
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
// if embedding.is_err() {
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
// }
// Ok(Embedding(embedding.unwrap()))
// }
// }
// impl ToSql for Embedding {
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
// let bytes = bincode::serialize(&self.0)
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
// }
// }
#[derive(Clone)]
pub struct OpenAIEmbeddings {
pub client: Arc<dyn HttpClient>,
@@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
request_timeout,
)
.await?;
request_number += 1;
match response.status() {

View File

@@ -22,8 +22,11 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
uuid.workspace = true
semantic_index = { path = "../semantic_index" }
project = { path = "../project" }
uuid.workspace = true
log.workspace = true
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
@@ -36,7 +39,7 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
tiktoken-rs = "0.5"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View File

@@ -1,7 +1,7 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
codegen::{self, Codegen, CodegenKind},
prompts::generate_content_prompt,
prompts::{generate_content_prompt, PromptCodeSnippet},
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
SavedMessage,
};
@@ -17,7 +17,7 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
},
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
};
use fs::Fs;
use futures::StreamExt;
@@ -29,13 +29,15 @@ use gpui::{
},
fonts::HighlightStyle,
geometry::vector::{vec2f, Vector2F},
platform::{CursorStyle, MouseButton},
platform::{CursorStyle, MouseButton, PromptLevel},
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
WeakModelHandle, WeakViewHandle, WindowContext,
};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
use project::Project;
use search::BufferSearchBar;
use semantic_index::{SemanticIndex, SemanticIndexStatus};
use settings::SettingsStore;
use std::{
cell::{Cell, RefCell},
@@ -46,7 +48,7 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
time::Duration,
time::{Duration, Instant},
};
use theme::{
components::{action_button::Button, ComponentExt},
@@ -72,6 +74,7 @@ actions!(
ResetKey,
InlineAssist,
ToggleIncludeConversation,
ToggleRetrieveContext,
]
);
@@ -108,6 +111,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(InlineAssistant::confirm);
cx.add_action(InlineAssistant::cancel);
cx.add_action(InlineAssistant::toggle_include_conversation);
cx.add_action(InlineAssistant::toggle_retrieve_context);
cx.add_action(InlineAssistant::move_up);
cx.add_action(InlineAssistant::move_down);
}
@@ -145,6 +149,8 @@ pub struct AssistantPanel {
include_conversation_in_next_inline_assist: bool,
inline_prompt_history: VecDeque<String>,
_watch_saved_conversations: Task<Result<()>>,
semantic_index: Option<ModelHandle<SemanticIndex>>,
retrieve_context_in_next_inline_assist: bool,
}
impl AssistantPanel {
@@ -191,6 +197,9 @@ impl AssistantPanel {
toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
toolbar
});
let semantic_index = SemanticIndex::global(cx);
let mut this = Self {
workspace: workspace_handle,
active_editor_index: Default::default(),
@@ -215,6 +224,8 @@ impl AssistantPanel {
include_conversation_in_next_inline_assist: false,
inline_prompt_history: Default::default(),
_watch_saved_conversations,
semantic_index,
retrieve_context_in_next_inline_assist: false,
};
let mut old_dock_position = this.position(cx);
@@ -262,12 +273,19 @@ impl AssistantPanel {
return;
};
let project = workspace.project();
this.update(cx, |assistant, cx| {
assistant.new_inline_assist(&active_editor, cx)
assistant.new_inline_assist(&active_editor, cx, project)
});
}
fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
fn new_inline_assist(
&mut self,
editor: &ViewHandle<Editor>,
cx: &mut ViewContext<Self>,
project: &ModelHandle<Project>,
) {
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
api_key
} else {
@@ -278,26 +296,61 @@ impl AssistantPanel {
if selection.start.excerpt_id() != selection.end.excerpt_id() {
return;
}
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
// Extend the selection to the start and the end of the line.
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
if point_selection.end > point_selection.start {
point_selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if point_selection.end.column == 0 {
point_selection.end.row -= 1;
}
point_selection.end.column = snapshot.line_len(point_selection.end.row);
}
let codegen_kind = if point_selection.start == point_selection.end {
CodegenKind::Generate {
position: snapshot.anchor_after(point_selection.start),
}
} else {
CodegenKind::Transform {
range: snapshot.anchor_before(point_selection.start)
..snapshot.anchor_after(point_selection.end),
}
};
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let provider = Arc::new(OpenAICompletionProvider::new(
api_key,
cx.background().clone(),
));
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
CodegenKind::Generate {
position: selection.start,
}
} else {
CodegenKind::Transform {
range: selection.start..selection.end,
}
};
let codegen = cx.add_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
if let Some(semantic_index) = self.semantic_index.clone() {
let project = project.clone();
cx.spawn(|_, mut cx| async move {
let previously_indexed = semantic_index
.update(&mut cx, |index, cx| {
index.project_previously_indexed(&project, cx)
})
.await
.unwrap_or(false);
if previously_indexed {
let _ = semantic_index
.update(&mut cx, |index, cx| {
index.index_project(project.clone(), cx)
})
.await;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
let inline_assistant = cx.add_view(|cx| {
let assistant = InlineAssistant::new(
@@ -308,6 +361,9 @@ impl AssistantPanel {
codegen.clone(),
self.workspace.clone(),
cx,
self.retrieve_context_in_next_inline_assist,
self.semantic_index.clone(),
project.clone(),
);
cx.focus_self();
assistant
@@ -319,7 +375,7 @@ impl AssistantPanel {
editor.insert_blocks(
[BlockProperties {
style: BlockStyle::Flex,
position: selection.head().bias_left(&snapshot),
position: snapshot.anchor_before(point_selection.head()),
height: 2,
render: Arc::new({
let inline_assistant = inline_assistant.clone();
@@ -348,6 +404,7 @@ impl AssistantPanel {
editor: editor.downgrade(),
inline_assistant: Some((block_id, inline_assistant.clone())),
codegen: codegen.clone(),
project: project.downgrade(),
_subscriptions: vec![
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
cx.subscribe(editor, {
@@ -426,8 +483,15 @@ impl AssistantPanel {
InlineAssistantEvent::Confirmed {
prompt,
include_conversation,
retrieve_context,
} => {
self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
self.confirm_inline_assist(
assist_id,
prompt,
*include_conversation,
cx,
*retrieve_context,
);
}
InlineAssistantEvent::Canceled => {
self.finish_inline_assist(assist_id, true, cx);
@@ -440,6 +504,9 @@ impl AssistantPanel {
} => {
self.include_conversation_in_next_inline_assist = *include_conversation;
}
InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
self.retrieve_context_in_next_inline_assist = *retrieve_context
}
}
}
@@ -518,6 +585,7 @@ impl AssistantPanel {
user_prompt: &str,
include_conversation: bool,
cx: &mut ViewContext<Self>,
retrieve_context: bool,
) {
let conversation = if include_conversation {
self.active_editor()
@@ -539,6 +607,8 @@ impl AssistantPanel {
return;
};
let project = pending_assist.project.clone();
self.inline_prompt_history
.retain(|prompt| prompt != user_prompt);
self.inline_prompt_history.push_back(user_prompt.into());
@@ -578,14 +648,63 @@ impl AssistantPanel {
let codegen_kind = codegen.read(cx).kind().clone();
let user_prompt = user_prompt.to_string();
let prompt = cx.background().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
});
let mut messages = Vec::new();
let snippets = if retrieve_context {
let Some(project) = project.upgrade(cx) else {
return;
};
let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
let search_results = semantic_index.update(cx, |this, cx| {
this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
});
cx.background()
.spawn(async move { search_results.await.unwrap_or_default() })
} else {
Task::ready(Vec::new())
};
let snippets = cx.spawn(|_, cx| async move {
let mut snippets = Vec::new();
for result in search_results.await {
snippets.push(PromptCodeSnippet::new(result, &cx));
// snippets.push(result.buffer.read_with(&cx, |buffer, _| {
// buffer
// .snapshot()
// .text_for_range(result.range)
// .collect::<String>()
// }));
}
snippets
});
snippets
} else {
Task::ready(Vec::new())
};
let mut model = settings::get::<AssistantSettings>(cx)
.default_open_ai_model
.clone();
let model_name = model.full_name();
let prompt = cx.background().spawn(async move {
let snippets = snippets.await;
let language_name = language_name.as_deref();
generate_content_prompt(
user_prompt,
language_name,
&buffer,
range,
codegen_kind,
snippets,
model_name,
)
});
let mut messages = Vec::new();
if let Some(conversation) = conversation {
let conversation = conversation.read(cx);
let buffer = conversation.buffer.read(cx);
@@ -1498,12 +1617,14 @@ impl Conversation {
Role::Assistant => "assistant".into(),
Role::System => "system".into(),
},
content: self
.buffer
.read(cx)
.text_for_range(message.offset_range)
.collect(),
content: Some(
self.buffer
.read(cx)
.text_for_range(message.offset_range)
.collect(),
),
name: None,
function_call: None,
})
})
.collect::<Vec<_>>();
@@ -2622,12 +2743,16 @@ enum InlineAssistantEvent {
Confirmed {
prompt: String,
include_conversation: bool,
retrieve_context: bool,
},
Canceled,
Dismissed,
IncludeConversationToggled {
include_conversation: bool,
},
RetrieveContextToggled {
retrieve_context: bool,
},
}
struct InlineAssistant {
@@ -2643,6 +2768,11 @@ struct InlineAssistant {
pending_prompt: String,
codegen: ModelHandle<Codegen>,
_subscriptions: Vec<Subscription>,
retrieve_context: bool,
semantic_index: Option<ModelHandle<SemanticIndex>>,
semantic_permissioned: Option<bool>,
project: WeakModelHandle<Project>,
maintain_rate_limit: Option<Task<()>>,
}
impl Entity for InlineAssistant {
@@ -2659,51 +2789,65 @@ impl View for InlineAssistant {
let theme = theme::current(cx);
Flex::row()
.with_child(
Flex::row()
.with_child(
Button::action(ToggleIncludeConversation)
.with_tooltip("Include Conversation", theme.tooltip.clone())
.with_children([Flex::row()
.with_child(
Button::action(ToggleIncludeConversation)
.with_tooltip("Include Conversation", theme.tooltip.clone())
.with_id(self.id)
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
.toggleable(self.include_conversation)
.with_style(theme.assistant.inline.include_conversation.clone())
.element()
.aligned(),
)
.with_children(if SemanticIndex::enabled(cx) {
Some(
Button::action(ToggleRetrieveContext)
.with_tooltip("Retrieve Context", theme.tooltip.clone())
.with_id(self.id)
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
.toggleable(self.include_conversation)
.with_style(theme.assistant.inline.include_conversation.clone())
.with_contents(theme::components::svg::Svg::new(
"icons/magnifying_glass.svg",
))
.toggleable(self.retrieve_context)
.with_style(theme.assistant.inline.retrieve_context.clone())
.element()
.aligned(),
)
.with_children(if let Some(error) = self.codegen.read(cx).error() {
Some(
Svg::new("icons/error.svg")
.with_color(theme.assistant.error_icon.color)
.constrained()
.with_width(theme.assistant.error_icon.width)
.contained()
.with_style(theme.assistant.error_icon.container)
.with_tooltip::<ErrorIcon>(
self.id,
error.to_string(),
None,
theme.tooltip.clone(),
cx,
)
.aligned(),
)
} else {
None
})
.aligned()
.constrained()
.dynamically({
let measurements = self.measurements.clone();
move |constraint, _, _| {
let measurements = measurements.get();
SizeConstraint {
min: vec2f(measurements.gutter_width, constraint.min.y()),
max: vec2f(measurements.gutter_width, constraint.max.y()),
}
} else {
None
})
.with_children(if let Some(error) = self.codegen.read(cx).error() {
Some(
Svg::new("icons/error.svg")
.with_color(theme.assistant.error_icon.color)
.constrained()
.with_width(theme.assistant.error_icon.width)
.contained()
.with_style(theme.assistant.error_icon.container)
.with_tooltip::<ErrorIcon>(
self.id,
error.to_string(),
None,
theme.tooltip.clone(),
cx,
)
.aligned(),
)
} else {
None
})
.aligned()
.constrained()
.dynamically({
let measurements = self.measurements.clone();
move |constraint, _, _| {
let measurements = measurements.get();
SizeConstraint {
min: vec2f(measurements.gutter_width, constraint.min.y()),
max: vec2f(measurements.gutter_width, constraint.max.y()),
}
}),
)
}
})])
.with_child(Empty::new().constrained().dynamically({
let measurements = self.measurements.clone();
move |constraint, _, _| {
@@ -2726,6 +2870,16 @@ impl View for InlineAssistant {
.left()
.flex(1., true),
)
.with_children(if self.retrieve_context {
Some(
Flex::row()
.with_children(self.retrieve_context_status(cx))
.flex(1., true)
.aligned(),
)
} else {
None
})
.contained()
.with_style(theme.assistant.inline.container)
.into_any()
@@ -2751,6 +2905,9 @@ impl InlineAssistant {
codegen: ModelHandle<Codegen>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
retrieve_context: bool,
semantic_index: Option<ModelHandle<SemanticIndex>>,
project: ModelHandle<Project>,
) -> Self {
let prompt_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
@@ -2764,11 +2921,16 @@ impl InlineAssistant {
editor.set_placeholder_text(placeholder, cx);
editor
});
let subscriptions = vec![
let mut subscriptions = vec![
cx.observe(&codegen, Self::handle_codegen_changed),
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
];
Self {
if let Some(semantic_index) = semantic_index.clone() {
subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
}
let assistant = Self {
id,
prompt_editor,
workspace,
@@ -2781,7 +2943,33 @@ impl InlineAssistant {
pending_prompt: String::new(),
codegen,
_subscriptions: subscriptions,
retrieve_context,
semantic_permissioned: None,
semantic_index,
project: project.downgrade(),
maintain_rate_limit: None,
};
assistant.index_project(cx).log_err();
assistant
}
fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
if let Some(value) = self.semantic_permissioned {
return Task::ready(Ok(value));
}
let Some(project) = self.project.upgrade(cx) else {
return Task::ready(Err(anyhow!("project was dropped")));
};
self.semantic_index
.as_ref()
.map(|semantic| {
semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
})
.unwrap_or(Task::ready(Ok(false)))
}
fn handle_prompt_editor_events(
@@ -2796,6 +2984,37 @@ impl InlineAssistant {
}
}
fn semantic_index_changed(
&mut self,
semantic_index: ModelHandle<SemanticIndex>,
cx: &mut ViewContext<Self>,
) {
let Some(project) = self.project.upgrade(cx) else {
return;
};
let status = semantic_index.read(cx).status(&project);
match status {
SemanticIndexStatus::Indexing {
rate_limit_expiry: Some(_),
..
} => {
if self.maintain_rate_limit.is_none() {
self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
loop {
cx.background().timer(Duration::from_secs(1)).await;
this.update(&mut cx, |_, cx| cx.notify()).log_err();
}
}));
}
return;
}
_ => {
self.maintain_rate_limit = None;
}
}
}
fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
let is_read_only = !self.codegen.read(cx).idle();
self.prompt_editor.update(cx, |editor, cx| {
@@ -2845,12 +3064,241 @@ impl InlineAssistant {
cx.emit(InlineAssistantEvent::Confirmed {
prompt,
include_conversation: self.include_conversation,
retrieve_context: self.retrieve_context,
});
self.confirmed = true;
cx.notify();
}
}
fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
let semantic_permissioned = self.semantic_permissioned(cx);
let Some(project) = self.project.upgrade(cx) else {
return;
};
let project_name = project
.read(cx)
.worktree_root_names(cx)
.collect::<Vec<&str>>()
.join("/");
let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0;
let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name,
if is_plural {
"s"
} else {""});
cx.spawn(|this, mut cx| async move {
// If Necessary prompt user
if !semantic_permissioned.await.unwrap_or(false) {
let mut answer = this.update(&mut cx, |_, cx| {
cx.prompt(
PromptLevel::Info,
prompt_text.as_str(),
&["Continue", "Cancel"],
)
})?;
if answer.next().await == Some(0) {
this.update(&mut cx, |this, _| {
this.semantic_permissioned = Some(true);
})?;
} else {
return anyhow::Ok(());
}
}
// If permissioned, update context appropriately
this.update(&mut cx, |this, cx| {
this.retrieve_context = !this.retrieve_context;
cx.emit(InlineAssistantEvent::RetrieveContextToggled {
retrieve_context: this.retrieve_context,
});
if this.retrieve_context {
this.index_project(cx).log_err();
}
cx.notify();
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
let Some(project) = self.project.upgrade(cx) else {
return Err(anyhow!("project was dropped!"));
};
let semantic_permissioned = self.semantic_permissioned(cx);
if let Some(semantic_index) = SemanticIndex::global(cx) {
cx.spawn(|_, mut cx| async move {
// This has to be updated to accomodate for semantic_permissions
if semantic_permissioned.await.unwrap_or(false) {
semantic_index
.update(&mut cx, |index, cx| index.index_project(project, cx))
.await
} else {
Err(anyhow!("project is not permissioned for semantic indexing"))
}
})
.detach_and_log_err(cx);
}
anyhow::Ok(())
}
fn retrieve_context_status(
&self,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<InlineAssistant>> {
enum ContextStatusIcon {}
let Some(project) = self.project.upgrade(cx) else {
return None;
};
if let Some(semantic_index) = SemanticIndex::global(cx) {
let status = semantic_index.update(cx, |index, _| index.status(&project));
let theme = theme::current(cx);
match status {
SemanticIndexStatus::NotAuthenticated {} => Some(
Svg::new("icons/error.svg")
.with_color(theme.assistant.error_icon.color)
.constrained()
.with_width(theme.assistant.error_icon.width)
.contained()
.with_style(theme.assistant.error_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
"Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.",
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
),
SemanticIndexStatus::NotIndexed {} => Some(
Svg::new("icons/error.svg")
.with_color(theme.assistant.inline.context_status.error_icon.color)
.constrained()
.with_width(theme.assistant.inline.context_status.error_icon.width)
.contained()
.with_style(theme.assistant.inline.context_status.error_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
"Not Indexed",
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
),
SemanticIndexStatus::Indexing {
remaining_files,
rate_limit_expiry,
} => {
let mut status_text = if remaining_files == 0 {
"Indexing...".to_string()
} else {
format!("Remaining files to index: {remaining_files}")
};
if let Some(rate_limit_expiry) = rate_limit_expiry {
let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
write!(
status_text,
" (rate limit expires in {}s)",
remaining_seconds.as_secs()
)
.unwrap();
}
}
Some(
Svg::new("icons/update.svg")
.with_color(theme.assistant.inline.context_status.in_progress_icon.color)
.constrained()
.with_width(theme.assistant.inline.context_status.in_progress_icon.width)
.contained()
.with_style(theme.assistant.inline.context_status.in_progress_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
status_text,
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
)
}
SemanticIndexStatus::Indexed {} => Some(
Svg::new("icons/check.svg")
.with_color(theme.assistant.inline.context_status.complete_icon.color)
.constrained()
.with_width(theme.assistant.inline.context_status.complete_icon.width)
.contained()
.with_style(theme.assistant.inline.context_status.complete_icon.container)
.with_tooltip::<ContextStatusIcon>(
self.id,
"Index up to date",
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.into_any(),
),
}
} else {
None
}
}
// fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
// let project = self.project.clone();
// if let Some(semantic_index) = self.semantic_index.clone() {
// let status = semantic_index.update(cx, |index, cx| index.status(&project));
// return match status {
// // This theoretically shouldnt be a valid code path
// // As the inline assistant cant be launched without an API key
// // We keep it here for safety
// semantic_index::SemanticIndexStatus::NotAuthenticated => {
// "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
// }
// semantic_index::SemanticIndexStatus::Indexed => {
// "Indexing Complete!".to_string()
// }
// semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
// let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
// if let Some(rate_limit_expiry) = rate_limit_expiry {
// let remaining_seconds =
// rate_limit_expiry.duration_since(Instant::now());
// if remaining_seconds > Duration::from_secs(0) {
// write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
// }
// }
// status
// }
// semantic_index::SemanticIndexStatus::NotIndexed => {
// "Not Indexed for Context Retrieval".to_string()
// }
// };
// }
// "".to_string()
// }
fn toggle_include_conversation(
&mut self,
_: &ToggleIncludeConversation,
@@ -2913,6 +3361,7 @@ struct PendingInlineAssist {
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
codegen: ModelHandle<Codegen>,
_subscriptions: Vec<Subscription>,
project: WeakModelHandle<Project>,
}
fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {

View File

@@ -1,9 +1,7 @@
use crate::streaming_diff::{Hunk, StreamingDiff};
use ai::completion::{CompletionProvider, OpenAIRequest};
use anyhow::Result;
use editor::{
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{Entity, ModelContext, ModelHandle, Task};
use language::{Rope, TransactionId};
@@ -40,26 +38,11 @@ impl Entity for Codegen {
impl Codegen {
pub fn new(
buffer: ModelHandle<MultiBuffer>,
mut kind: CodegenKind,
kind: CodegenKind,
provider: Arc<dyn CompletionProvider>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
match &mut kind {
CodegenKind::Transform { range } => {
let mut point_range = range.to_point(&snapshot);
point_range.start.column = 0;
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
point_range.end.column = snapshot.line_len(point_range.end.row);
}
range.start = snapshot.anchor_before(point_range.start);
range.end = snapshot.anchor_after(point_range.end);
}
CodegenKind::Generate { position } => {
*position = position.bias_right(&snapshot);
}
}
Self {
provider,
buffer: buffer.clone(),
@@ -386,7 +369,7 @@ mod tests {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let provider = Arc::new(TestCompletionProvider::new());
let codegen = cx.add_model(|cx| {

View File

@@ -1,9 +1,62 @@
use crate::codegen::CodegenKind;
use gpui::AsyncAppContext;
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
use semantic_index::SearchResult;
use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::ops::Range;
use std::path::PathBuf;
use tiktoken_rs::ChatCompletionRequestMessage;
pub struct PromptCodeSnippet {
path: Option<PathBuf>,
language_name: Option<String>,
content: String,
}
impl PromptCodeSnippet {
pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self {
let (content, language_name, file_path) =
search_result.buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let content = snapshot
.text_for_range(search_result.range.clone())
.collect::<String>();
let language_name = buffer
.language()
.and_then(|language| Some(language.name().to_string()));
let file_path = buffer
.file()
.and_then(|file| Some(file.path().to_path_buf()));
(content, language_name, file_path)
});
PromptCodeSnippet {
path: file_path,
language_name,
content,
}
}
}
impl ToString for PromptCodeSnippet {
fn to_string(&self) -> String {
let path = self
.path
.as_ref()
.and_then(|path| Some(path.to_string_lossy().to_string()))
.unwrap_or("".to_string());
let language_name = self.language_name.clone().unwrap_or("".to_string());
let content = self.content.clone();
format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
}
}
#[allow(dead_code)]
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
#[derive(Debug)]
struct Match {
@@ -120,70 +173,135 @@ pub fn generate_content_prompt(
buffer: &BufferSnapshot,
range: Range<impl ToOffset>,
kind: CodegenKind,
search_results: Vec<PromptCodeSnippet>,
model: &str,
) -> String {
let mut prompt = String::new();
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
const RESERVED_TOKENS_FOR_GENERATION: usize = 1000;
let mut prompts = Vec::new();
let range = range.to_offset(buffer);
// General Preamble
if let Some(language_name) = language_name {
writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
prompts.push(format!("You're an expert {language_name} engineer.\n"));
} else {
writeln!(prompt, "You're an expert engineer.\n").unwrap();
prompts.push("You're an expert engineer.\n".to_string());
}
let outline = summarize(buffer, range);
writeln!(
prompt,
"The file you are currently working on has the following outline:"
)
.unwrap();
// Snippets
let mut snippet_position = prompts.len() - 1;
let mut content = String::new();
content.extend(buffer.text_for_range(0..range.start));
if range.start == range.end {
content.push_str("<|START|>");
} else {
content.push_str("<|START|");
}
content.extend(buffer.text_for_range(range.clone()));
if range.start != range.end {
content.push_str("|END|>");
}
content.extend(buffer.text_for_range(range.end..buffer.len()));
prompts.push("The file you are currently working on has the following content:\n".to_string());
if let Some(language_name) = language_name {
let language_name = language_name.to_lowercase();
writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
prompts.push(format!("```{language_name}\n{content}\n```"));
} else {
writeln!(prompt, "```\n{outline}\n```").unwrap();
prompts.push(format!("```\n{content}\n```"));
}
match kind {
CodegenKind::Generate { position: _ } => {
writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
writeln!(
prompt,
"Assume the cursor is located where the `<|START|` marker is."
)
.unwrap();
writeln!(
prompt,
prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string());
prompts
.push("Assume the cursor is located where the `<|START|` marker is.".to_string());
prompts.push(
"Text can't be replaced, so assume your answer will be inserted at the cursor."
)
.unwrap();
writeln!(
prompt,
.to_string(),
);
prompts.push(format!(
"Generate text based on the users prompt: {user_prompt}"
)
.unwrap();
));
}
CodegenKind::Transform { range: _ } => {
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
writeln!(
prompt,
"Modify the users code selected text based upon the users prompt: {user_prompt}"
)
.unwrap();
writeln!(
prompt,
"You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
)
.unwrap();
prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string());
prompts.push(format!(
"Modify the users code selected text based upon the users prompt: '{user_prompt}'"
));
prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string());
}
}
if let Some(language_name) = language_name {
writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
prompts.push(format!(
"Your answer MUST always and only be valid {language_name}"
));
}
writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
writeln!(prompt, "Never make remarks about the output.").unwrap();
prompts.push("Never make remarks about the output.".to_string());
prompts.push("Do not return any text, except the generated code.".to_string());
prompts.push("Always wrap your code in a Markdown block".to_string());
prompt
let current_messages = [ChatCompletionRequestMessage {
role: "user".to_string(),
content: Some(prompts.join("\n")),
function_call: None,
name: None,
}];
let mut remaining_token_count = if let Ok(current_token_count) =
tiktoken_rs::num_tokens_from_messages(model, &current_messages)
{
let max_token_count = tiktoken_rs::model::get_context_size(model);
let intermediate_token_count = if max_token_count > current_token_count {
max_token_count - current_token_count
} else {
0
};
if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION {
0
} else {
intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION
}
} else {
// If tiktoken fails to count token count, assume we have no space remaining.
0
};
// TODO:
// - add repository name to snippet
// - add file path
// - add language
if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) {
let mut template = "You are working inside a large repository, here are a few code snippets that may be useful";
for search_result in search_results {
let mut snippet_prompt = template.to_string();
let snippet = search_result.to_string();
writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap();
let token_count = encoding
.encode_with_special_tokens(snippet_prompt.as_str())
.len();
if token_count <= remaining_token_count {
if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT {
prompts.insert(snippet_position, snippet_prompt);
snippet_position += 1;
remaining_token_count -= token_count;
// If you have already added the template to the prompt, remove the template.
template = "";
}
} else {
break;
}
}
}
prompts.join("\n")
}
#[cfg(test)]

View File

@@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }

View File

@@ -5,7 +5,6 @@ pub mod room;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use channel::ChannelId;
use client::{
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
@@ -79,7 +78,7 @@ impl ActiveCall {
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}

View File

@@ -18,7 +18,7 @@ use live_kit_client::{
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
RemoteVideoTrackUpdate,
};
use postage::stream::Stream;
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
@@ -70,6 +70,8 @@ pub struct Room {
user_store: ModelHandle<UserStore>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
room_update_completed_tx: watch::Sender<Option<()>>,
room_update_completed_rx: watch::Receiver<Option<()>>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
}
@@ -211,6 +213,8 @@ impl Room {
Audio::play_sound(Sound::Joined, cx);
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
Self {
id,
channel_id,
@@ -230,6 +234,8 @@ impl Room {
user_store,
follows_by_leader_id_project_id: Default::default(),
maintain_connection: Some(maintain_connection),
room_update_completed_tx,
room_update_completed_rx,
}
}
@@ -599,28 +605,40 @@ impl Room {
}
/// Returns the most 'active' projects, defined as most people in the project
pub fn most_active_project(&self) -> Option<(u64, u64)> {
let mut projects = HashMap::default();
let mut hosts = HashMap::default();
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
for participant in self.remote_participants.values() {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
*projects.entry(project_id).or_insert(0) += 1;
project_hosts_and_guest_counts
.entry(project_id)
.or_default()
.1 += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
*projects.entry(project.id).or_insert(0) += 1;
hosts.insert(project.id, participant.user.id);
project_hosts_and_guest_counts
.entry(project.id)
.or_default()
.0 = Some(participant.user.id);
}
}
let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
pairs.sort_by_key(|(_, count)| *count as i32);
if let Some(user) = self.user_store.read(cx).current_user() {
for project in &self.local_participant.projects {
project_hosts_and_guest_counts
.entry(project.id)
.or_default()
.0 = Some(user.id);
}
}
pairs
.first()
.map(|(project_id, _)| (*project_id, hosts[&project_id]))
project_hosts_and_guest_counts
.into_iter()
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
.max_by_key(|(_, _, guest_count)| *guest_count)
.map(|(id, host, _)| (id, host))
}
async fn handle_room_updated(
@@ -686,6 +704,7 @@ impl Room {
let Some(peer_id) = participant.peer_id else {
continue;
};
let participant_index = ParticipantIndex(participant.participant_index);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
@@ -736,8 +755,9 @@ impl Room {
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
remote_participant.projects = participant.projects;
remote_participant.participant_index = participant_index;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -749,9 +769,7 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
participant_index: ParticipantIndex(
participant.participant_index,
),
participant_index,
peer_id,
projects: participant.projects,
location,
@@ -855,6 +873,7 @@ impl Room {
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
});
}));
@@ -863,6 +882,17 @@ impl Room {
Ok(())
}
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
let mut done_rx = self.room_update_completed_rx.clone();
async move {
while let Some(result) = done_rx.next().await {
if result.is_some() {
break;
}
}
}
}
fn remote_video_track_updated(
&mut self,
change: RemoteVideoTrackUpdate,

View File

@@ -2,19 +2,21 @@ mod channel_buffer;
mod channel_chat;
mod channel_store;
use client::{Client, UserStore};
use gpui::{AppContext, ModelHandle};
use std::sync::Arc;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
pub use channel_store::{
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
use client::Client;
use std::sync::Arc;
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc<Client>) {
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}

View File

@@ -99,6 +99,10 @@ impl ChannelBuffer {
}))
}
pub fn remote_id(&self, cx: &AppContext) -> u64 {
self.buffer.read(cx).remote_id()
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.user_store
}

View File

@@ -2,19 +2,25 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{Client, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use rpc::{
proto::{self, ChannelEdge, ChannelPermission},
proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility},
TypedEnvelope,
};
use serde_derive::{Deserialize, Serialize};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt;
use self::channel_index::ChannelIndex;
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.set_global(channel_store);
}
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -43,17 +49,63 @@ pub type ChannelData = (Channel, ChannelPath);
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub visibility: proto::ChannelVisibility,
pub unseen_note_version: Option<(u64, clock::Global)>,
pub unseen_message_id: Option<u64>,
}
impl Channel {
pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned()
+ "channel/"
+ &self.slug()
+ "-"
+ &self.id.to_string()
}
pub fn slug(&self) -> String {
let slug: String = self
.name
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
slug.trim_matches(|c| c == '-').to_string()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ChannelPath(Arc<[ChannelId]>);
pub struct ChannelMembership {
pub user: Arc<User>,
pub kind: proto::channel_member::Kind,
pub admin: bool,
pub role: proto::ChannelRole,
}
impl ChannelMembership {
pub fn sort_key(&self) -> MembershipSortKey {
MembershipSortKey {
role_order: match self.role {
proto::ChannelRole::Admin => 0,
proto::ChannelRole::Member => 1,
proto::ChannelRole::Banned => 2,
proto::ChannelRole::Guest => 3,
},
kind_order: match self.kind {
proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::AncestorMember => 1,
proto::channel_member::Kind::Invitee => 2,
},
username_order: self.user.github_login.as_str(),
}
}
}
#[derive(PartialOrd, Ord, PartialEq, Eq)]
pub struct MembershipSortKey<'a> {
role_order: u8,
kind_order: u8,
username_order: &'a str,
}
pub enum ChannelEvent {
@@ -71,6 +123,10 @@ enum OpenedModelHandle<E: Entity> {
}
impl ChannelStore {
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
@@ -84,12 +140,21 @@ impl ChannelStore {
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade(&cx)?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
}
}
if status.is_connected() {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
} else {
this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
}
}
Some(())
@@ -406,7 +471,7 @@ impl ChannelStore {
insert_edge: parent_edge,
channel_permissions: vec![ChannelPermission {
channel_id,
is_admin: true,
role: ChannelRole::Admin.into(),
}],
..Default::default()
},
@@ -478,11 +543,30 @@ impl ChannelStore {
})
}
pub fn set_channel_visibility(
&mut self,
channel_id: ChannelId,
visibility: ChannelVisibility,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(|_, _| async move {
let _ = client
.request(proto::SetChannelVisibility {
channel_id,
visibility: visibility.into(),
})
.await?;
Ok(())
})
}
pub fn invite_member(
&mut self,
channel_id: ChannelId,
user_id: UserId,
admin: bool,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
@@ -496,7 +580,7 @@ impl ChannelStore {
.request(proto::InviteChannelMember {
channel_id,
user_id,
admin,
role: role.into(),
})
.await;
@@ -540,11 +624,11 @@ impl ChannelStore {
})
}
pub fn set_member_admin(
pub fn set_member_role(
&mut self,
channel_id: ChannelId,
user_id: UserId,
admin: bool,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
@@ -555,10 +639,10 @@ impl ChannelStore {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
let result = client
.request(proto::SetChannelMemberAdmin {
.request(proto::SetChannelMemberRole {
channel_id,
user_id,
admin,
role: role.into(),
})
.await;
@@ -646,8 +730,8 @@ impl ChannelStore {
.filter_map(|(user, member)| {
Some(ChannelMembership {
user,
admin: member.admin,
kind: proto::channel_member::Kind::from_i32(member.kind)?,
role: member.role(),
kind: member.kind(),
})
})
.collect())
@@ -793,7 +877,7 @@ impl ChannelStore {
})
}
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@@ -804,7 +888,10 @@ impl ChannelStore {
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(RECONNECT_TIMEOUT).await;
if wait_for_reconnect {
cx.background().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
for (_, buffer) in this.opened_buffers.drain() {
@@ -839,6 +926,7 @@ impl ChannelStore {
ix,
Arc::new(Channel {
id: channel.id,
visibility: channel.visibility(),
name: channel.name,
unseen_note_version: None,
unseen_message_id: None,
@@ -905,7 +993,7 @@ impl ChannelStore {
}
for permission in payload.channel_permissions {
if permission.is_admin {
if permission.role() == proto::ChannelRole::Admin {
self.channels_with_admin_privileges
.insert(permission.channel_id);
} else {

View File

@@ -123,12 +123,15 @@ impl<'a> ChannelPathsInsertGuard<'a> {
pub fn insert(&mut self, channel_proto: proto::Channel) {
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
Arc::make_mut(existing_channel).name = channel_proto.name;
let existing_channel = Arc::make_mut(existing_channel);
existing_channel.visibility = channel_proto.visibility();
existing_channel.name = channel_proto.name;
} else {
self.channels_by_id.insert(
channel_proto.id,
Arc::new(Channel {
id: channel_proto.id,
visibility: channel_proto.visibility(),
name: channel_proto.name,
unseen_note_version: None,
unseen_message_id: None,

View File

@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use gpui::{AppContext, ModelHandle, TestAppContext};
use rpc::proto;
use rpc::proto::{self};
use settings::SettingsStore;
use util::http::FakeHttpClient;
@@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) {
proto::Channel {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
proto::Channel {
id: 2,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
],
channel_permissions: vec![proto::ChannelPermission {
channel_id: 1,
is_admin: true,
role: proto::ChannelRole::Admin.into(),
}],
..Default::default()
},
@@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) {
proto::Channel {
id: 3,
name: "x".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
proto::Channel {
id: 4,
name: "y".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
],
insert_edge: vec![
@@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
proto::Channel {
id: 0,
name: "a".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
proto::Channel {
id: 1,
name: "b".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
proto::Channel {
id: 2,
name: "c".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
},
],
insert_edge: vec![
@@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
],
channel_permissions: vec![proto::ChannelPermission {
channel_id: 0,
is_admin: true,
role: proto::ChannelRole::Admin.into(),
}],
..Default::default()
},
@@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
channels: vec![proto::Channel {
id: channel_id,
name: "the-channel".to_string(),
visibility: proto::ChannelVisibility::Members as i32,
}],
..Default::default()
});
@@ -340,10 +348,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
crate::init(&client);
client::init(&client, cx);
crate::init(&client, user_store, cx);
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
ChannelStore::global(cx)
}
fn update_channels(

View File

@@ -182,6 +182,7 @@ impl Bundle {
kCFStringEncodingUTF8,
ptr::null(),
));
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {

View File

@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]);
actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
}
}
});
cx.add_global_action({
let client = client.clone();
move |_: &Reconnect, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.reconnect(&cx);
})
.detach();
}
}
});
}
pub struct Client {
@@ -1212,6 +1223,11 @@ impl Client {
self.set_status(Status::SignedOut, cx);
}
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::ConnectionLost, cx);
}
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)

View File

@@ -4,11 +4,12 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
use uuid::Uuid;
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
@@ -20,7 +21,7 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
session_id: String, // Per app launch
session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
@@ -43,7 +44,7 @@ lazy_static! {
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
session_id: String,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
os_name: &'static str,
@@ -134,7 +135,7 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
session_id: Uuid::new_v4().to_string(),
session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
@@ -149,9 +150,15 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
pub fn start(
self: &Arc<Self>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
@@ -161,8 +168,16 @@ impl Telemetry {
let this = self.clone();
cx.spawn(|mut cx| async move {
let mut system = System::new_all();
system.refresh_all();
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory() // For memory usage
.with_processes(ProcessRefreshKind::everything()) // For process usage
.with_cpu(CpuRefreshKind::everything()); // For core count
let mut system = System::new_with_specifics(refresh_kind);
// Avoiding calling `refresh_all()`, just update what we need
system.refresh_specifics(refresh_kind);
loop {
// Waiting some amount of time before the first query is important to get a reasonable value
@@ -170,8 +185,7 @@ impl Telemetry {
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_memory();
system.refresh_processes();
system.refresh_specifics(refresh_kind);
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {
@@ -283,23 +297,21 @@ impl Telemetry {
{
let state = this.state.lock();
json_bytes.clear();
serde_json::to_writer(
&mut json_bytes,
&ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
architecture: state.architecture,
let request_body = ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
architecture: state.architecture,
release_channel: state.release_channel,
events,
},
)?;
release_channel: state.release_channel,
events,
};
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.23.2"
version = "0.24.0"
publish = false
[[bin]]
@@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
node_runtime = { path = "../node_runtime" }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }

View File

@@ -37,12 +37,14 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"live_kit_room" VARCHAR NOT NULL,
"enviroment" VARCHAR,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
@@ -190,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
CREATE TABLE "channels" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now
"created_at" TIMESTAMP NOT NULL DEFAULT now,
"visibility" VARCHAR NOT NULL
);
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
@@ -224,6 +227,7 @@ CREATE TABLE "channel_members" (
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"admin" BOOLEAN NOT NULL DEFAULT false,
"role" VARCHAR,
"accepted" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP NOT NULL DEFAULT now
);

View File

@@ -0,0 +1 @@
ALTER TABLE rooms ADD COLUMN enviroment TEXT;

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");

View File

@@ -0,0 +1,4 @@
ALTER TABLE channel_members ADD COLUMN role TEXT;
UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END;
ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members';

View File

@@ -0,0 +1,8 @@
-- Add migration script here
ALTER TABLE projects
DROP CONSTRAINT projects_room_id_fkey,
ADD CONSTRAINT projects_room_id_fkey
FOREIGN KEY (room_id)
REFERENCES rooms (id)
ON DELETE CASCADE;

View File

@@ -432,6 +432,7 @@ pub struct NewUserResult {
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
}
#[derive(Debug, PartialEq)]

View File

@@ -1,4 +1,5 @@
use crate::Result;
use rpc::proto;
use sea_orm::{entity::prelude::*, DbErr};
use serde::{Deserialize, Serialize};
@@ -80,3 +81,101 @@ id_type!(SignupId);
id_type!(UserId);
id_type!(ChannelBufferCollaboratorId);
id_type!(FlagId);
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
#[sea_orm(rs_type = "String", db_type = "String(None)")]
pub enum ChannelRole {
#[sea_orm(string_value = "admin")]
Admin,
#[sea_orm(string_value = "member")]
#[default]
Member,
#[sea_orm(string_value = "guest")]
Guest,
#[sea_orm(string_value = "banned")]
Banned,
}
impl ChannelRole {
pub fn should_override(&self, other: Self) -> bool {
use ChannelRole::*;
match self {
Admin => matches!(other, Member | Banned | Guest),
Member => matches!(other, Banned | Guest),
Banned => matches!(other, Guest),
Guest => false,
}
}
pub fn max(&self, other: Self) -> Self {
if self.should_override(other) {
*self
} else {
other
}
}
}
impl From<proto::ChannelRole> for ChannelRole {
fn from(value: proto::ChannelRole) -> Self {
match value {
proto::ChannelRole::Admin => ChannelRole::Admin,
proto::ChannelRole::Member => ChannelRole::Member,
proto::ChannelRole::Guest => ChannelRole::Guest,
proto::ChannelRole::Banned => ChannelRole::Banned,
}
}
}
impl Into<proto::ChannelRole> for ChannelRole {
fn into(self) -> proto::ChannelRole {
match self {
ChannelRole::Admin => proto::ChannelRole::Admin,
ChannelRole::Member => proto::ChannelRole::Member,
ChannelRole::Guest => proto::ChannelRole::Guest,
ChannelRole::Banned => proto::ChannelRole::Banned,
}
}
}
impl Into<i32> for ChannelRole {
fn into(self) -> i32 {
let proto: proto::ChannelRole = self.into();
proto.into()
}
}
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
#[sea_orm(rs_type = "String", db_type = "String(None)")]
pub enum ChannelVisibility {
#[sea_orm(string_value = "public")]
Public,
#[sea_orm(string_value = "members")]
#[default]
Members,
}
impl From<proto::ChannelVisibility> for ChannelVisibility {
fn from(value: proto::ChannelVisibility) -> Self {
match value {
proto::ChannelVisibility::Public => ChannelVisibility::Public,
proto::ChannelVisibility::Members => ChannelVisibility::Members,
}
}
}
impl Into<proto::ChannelVisibility> for ChannelVisibility {
fn into(self) -> proto::ChannelVisibility {
match self {
ChannelVisibility::Public => proto::ChannelVisibility::Public,
ChannelVisibility::Members => proto::ChannelVisibility::Members,
}
}
}
impl Into<i32> for ChannelVisibility {
fn into(self) -> i32 {
let proto: proto::ChannelVisibility = self.into();
proto.into()
}
}

View File

@@ -482,7 +482,9 @@ impl Database {
)
.await?;
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
channel_members = self
.get_channel_participants_internal(channel_id, &*tx)
.await?;
let collaborators = self
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
.await?;

File diff suppressed because it is too large Load Diff

View File

@@ -89,7 +89,7 @@ impl Database {
let mut rows = channel_message::Entity::find()
.filter(condition)
.order_by_asc(channel_message::Column::Id)
.order_by_desc(channel_message::Column::Id)
.limit(count as u64)
.stream(&*tx)
.await?;
@@ -110,6 +110,7 @@ impl Database {
});
}
drop(rows);
messages.reverse();
Ok(messages)
})
.await
@@ -179,7 +180,9 @@ impl Database {
)
.await?;
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
let mut channel_members = self
.get_channel_participants_internal(channel_id, &*tx)
.await?;
channel_members.retain(|member| !participant_user_ids.contains(member));
Ok((
@@ -336,8 +339,22 @@ impl Database {
.filter(channel_message::Column::SenderId.eq(user_id))
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("no such message"))?;
if self
.check_user_is_channel_admin(channel_id, user_id, &*tx)
.await
.is_ok()
{
let result = channel_message::Entity::delete_by_id(message_id)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("no such message"))?;
}
} else {
Err(anyhow!("operation could not be completed"))?;
}
}
Ok(participant_connection_ids)

View File

@@ -53,7 +53,9 @@ impl Database {
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
let channel_members;
if let Some(channel_id) = channel_id {
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
channel_members = self
.get_channel_participants_internal(channel_id, &tx)
.await?;
} else {
channel_members = Vec::new();
@@ -107,10 +109,12 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
release_channel: &str,
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(live_kit_room.into()),
enviroment: ActiveValue::set(Some(release_channel.to_string())),
..Default::default()
}
.insert(&*tx)
@@ -270,112 +274,165 @@ impl Database {
room_id: RoomId,
user_id: UserId,
connection: ConnectionId,
enviroment: &str,
) -> Result<RoomGuard<JoinRoom>> {
self.room_transaction(room_id, |tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelId {
enum QueryChannelIdAndEnviroment {
ChannelId,
Enviroment,
}
let channel_id: Option<ChannelId> = room::Entity::find()
.select_only()
.column(room::Column::ChannelId)
.filter(room::Column::Id.eq(room_id))
.into_values::<_, QueryChannelId>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryParticipantIndices {
ParticipantIndex,
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
room::Entity::find()
.select_only()
.column(room::Column::ChannelId)
.column(room::Column::Enviroment)
.filter(room::Column::Id.eq(room_id))
.into_values::<_, QueryChannelIdAndEnviroment>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if let Some(release_channel) = release_channel {
if &release_channel != enviroment {
Err(anyhow!("must join using the {} release", release_channel))?;
}
}
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
.filter(
room_participant::Column::RoomId
.eq(room_id)
.and(room_participant::Column::ParticipantIndex.is_not_null()),
)
.select_only()
.column(room_participant::Column::ParticipantIndex)
.into_values::<_, QueryParticipantIndices>()
.all(&*tx)
if channel_id.is_some() {
Err(anyhow!("tried to join channel call directly"))?
}
let participant_index = self
.get_next_participant_index_internal(room_id, &*tx)
.await?;
let mut participant_index = 0;
while existing_participant_indices.contains(&participant_index) {
participant_index += 1;
}
if let Some(channel_id) = channel_id {
self.check_user_is_channel_member(channel_id, user_id, &*tx)
.await?;
room_participant::Entity::insert_many([room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(user_id),
let result = room_participant::Entity::update_many()
.filter(
Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::UserId.eq(user_id))
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.set(room_participant::ActiveModel {
participant_index: ActiveValue::Set(Some(participant_index)),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
calling_user_id: ActiveValue::set(user_id),
calling_connection_id: ActiveValue::set(connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
participant_index: ActiveValue::Set(Some(participant_index)),
..Default::default()
}])
.on_conflict(
OnConflict::columns([room_participant::Column::UserId])
.update_columns([
room_participant::Column::AnsweringConnectionId,
room_participant::Column::AnsweringConnectionServerId,
room_participant::Column::AnsweringConnectionLost,
room_participant::Column::ParticipantIndex,
])
.to_owned(),
)
})
.exec(&*tx)
.await?;
} else {
let result = room_participant::Entity::update_many()
.filter(
Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::UserId.eq(user_id))
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.set(room_participant::ActiveModel {
participant_index: ActiveValue::Set(Some(participant_index)),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
..Default::default()
})
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("room does not exist or was already joined"))?;
}
if result.rows_affected == 0 {
Err(anyhow!("room does not exist or was already joined"))?;
}
let room = self.get_room(room_id, &tx).await?;
let channel_members = if let Some(channel_id) = channel_id {
self.get_channel_members_internal(channel_id, &tx).await?
} else {
Vec::new()
};
Ok(JoinRoom {
room,
channel_id,
channel_members,
channel_id: None,
channel_members: vec![],
})
})
.await
}
async fn get_next_participant_index_internal(
&self,
room_id: RoomId,
tx: &DatabaseTransaction,
) -> Result<i32> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryParticipantIndices {
ParticipantIndex,
}
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
.filter(
room_participant::Column::RoomId
.eq(room_id)
.and(room_participant::Column::ParticipantIndex.is_not_null()),
)
.select_only()
.column(room_participant::Column::ParticipantIndex)
.into_values::<_, QueryParticipantIndices>()
.all(&*tx)
.await?;
let mut participant_index = 0;
while existing_participant_indices.contains(&participant_index) {
participant_index += 1;
}
Ok(participant_index)
}
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
self.transaction(|tx| async move {
let room: Option<room::Model> = room::Entity::find()
.filter(room::Column::Id.eq(room_id))
.one(&*tx)
.await?;
Ok(room.and_then(|room| room.channel_id))
})
.await
}
pub(crate) async fn join_channel_room_internal(
&self,
channel_id: ChannelId,
room_id: RoomId,
user_id: UserId,
connection: ConnectionId,
tx: &DatabaseTransaction,
) -> Result<JoinRoom> {
let participant_index = self
.get_next_participant_index_internal(room_id, &*tx)
.await?;
room_participant::Entity::insert_many([room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(user_id),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
calling_user_id: ActiveValue::set(user_id),
calling_connection_id: ActiveValue::set(connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
participant_index: ActiveValue::Set(Some(participant_index)),
..Default::default()
}])
.on_conflict(
OnConflict::columns([room_participant::Column::UserId])
.update_columns([
room_participant::Column::AnsweringConnectionId,
room_participant::Column::AnsweringConnectionServerId,
room_participant::Column::AnsweringConnectionLost,
room_participant::Column::ParticipantIndex,
])
.to_owned(),
)
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
let channel_members = self
.get_channel_participants_internal(channel_id, &tx)
.await?;
Ok(JoinRoom {
room,
channel_id: Some(channel_id),
channel_members,
})
}
pub async fn rejoin_room(
&self,
rejoin_room: proto::RejoinRoom,
@@ -667,7 +724,8 @@ impl Database {
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
let channel_members = if let Some(channel_id) = channel_id {
self.get_channel_members_internal(channel_id, &tx).await?
self.get_channel_participants_internal(channel_id, &tx)
.await?
} else {
Vec::new()
};
@@ -818,17 +876,15 @@ impl Database {
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
let deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id)
.filter(room::Column::ChannelId.is_null())
.exec(&*tx)
.await?;
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
result.rows_affected > 0
} else {
false
};
let channel_members = if let Some(channel_id) = channel_id {
self.get_channel_members_internal(channel_id, &tx).await?
self.get_channel_participants_internal(channel_id, &tx)
.await?
} else {
Vec::new()
};

View File

@@ -1,4 +1,4 @@
use crate::db::ChannelId;
use crate::db::{ChannelId, ChannelVisibility};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
@@ -7,6 +7,7 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,7 +1,7 @@
use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_members")]
pub struct Model {
#[sea_orm(primary_key)]
@@ -9,7 +9,7 @@ pub struct Model {
pub channel_id: ChannelId,
pub user_id: UserId,
pub accepted: bool,
pub admin: bool,
pub role: ChannelRole,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -8,6 +8,7 @@ pub struct Model {
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
pub enviroment: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use std::sync::Arc;
const TEST_RELEASE_CHANNEL: &'static str = "test";
pub struct TestDb {
pub db: Option<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
@@ -157,6 +159,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)
graph.channels.push(Channel {
id: *id,
name: name.to_string(),
visibility: ChannelVisibility::Members,
})
}

View File

@@ -54,9 +54,9 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
db.invite_channel_member(zed_id, b_id, a_id, false)
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
.await
.unwrap();
@@ -141,7 +141,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
assert_eq!(left_buffer.connections, &[connection_id_a],);
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
let _ = db
.join_channel_buffer(cargo_id, a_id, connection_id_a)
.await
@@ -207,11 +207,11 @@ async fn test_channel_buffers_last_operations(db: &Database) {
let mut text_buffers = Vec::new();
for i in 0..3 {
let channel = db
.create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id)
.create_root_channel(&format!("channel-{i}"), user_id)
.await
.unwrap();
db.invite_channel_member(channel, observer_id, user_id, false)
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
.await
.unwrap();
db.respond_to_channel_invite(channel, observer_id, true)

View File

@@ -5,10 +5,17 @@ use rpc::{
};
use crate::{
db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
db::{
queries::channels::ChannelGraph,
tests::{graph, TEST_RELEASE_CHANNEL},
ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
},
test_both_dbs,
};
use std::sync::Arc;
use std::sync::{
atomic::{AtomicI32, Ordering},
Arc,
};
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
@@ -41,12 +48,12 @@ async fn test_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
// Make sure that people cannot read channels they haven't been invited to
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
assert!(db.get_channel(zed_id, b_id).await.is_err());
db.invite_channel_member(zed_id, b_id, a_id, false)
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
.await
.unwrap();
@@ -54,16 +61,13 @@ async fn test_channels(db: &Arc<Database>) {
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.create_channel("livestreaming", Some(zed_id), a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.create_channel("replace", Some(zed_id), a_id)
.await
.unwrap();
@@ -71,14 +75,14 @@ async fn test_channels(db: &Arc<Database>) {
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.create_channel("cargo", Some(rust_id), a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.create_channel("cargo-ra", Some(cargo_id), a_id)
.await
.unwrap();
@@ -124,9 +128,13 @@ async fn test_channels(db: &Arc<Database>) {
);
// Update member permissions
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
let set_subchannel_admin = db
.set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
.await;
assert!(set_subchannel_admin.is_err());
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
let set_channel_admin = db
.set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
.await;
assert!(set_channel_admin.is_ok());
let result = db.get_channels_for_user(b_id).await.unwrap();
@@ -149,7 +157,7 @@ async fn test_channels(db: &Arc<Database>) {
// Remove a single channel
db.delete_channel(crdb_id, a_id).await.unwrap();
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(crdb_id, a_id).await.is_err());
// Remove a channel tree
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
@@ -157,9 +165,9 @@ async fn test_channels(db: &Arc<Database>) {
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
assert_eq!(user_ids, &[a_id]);
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
assert!(db.get_channel(rust_id, a_id).await.is_err());
assert!(db.get_channel(cargo_id, a_id).await.is_err());
assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
}
test_both_dbs!(
@@ -198,23 +206,30 @@ async fn test_joining_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
// can join a room with membership to its channel
let joined_room = db
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
let (joined_room, _) = db
.join_channel(
channel_1,
user_1,
ConnectionId { owner_id, id: 1 },
TEST_RELEASE_CHANNEL,
)
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
let room_id = RoomId::from_proto(joined_room.room.id);
drop(joined_room);
// cannot join a room without membership to its channel
assert!(db
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
.join_room(
room_id,
user_2,
ConnectionId { owner_id, id: 1 },
TEST_RELEASE_CHANNEL
)
.await
.is_err());
}
@@ -228,64 +243,21 @@ test_both_dbs!(
async fn test_channel_invites(db: &Arc<Database>) {
db.create_server("test").await.unwrap();
let user_1 = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_2 = db
.create_user(
"user2@example.com",
false,
NewUserParams {
github_login: "user2".into(),
github_user_id: 6,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let user_1 = new_test_user(db, "user1@example.com").await;
let user_2 = new_test_user(db, "user2@example.com").await;
let user_3 = new_test_user(db, "user3@example.com").await;
let user_3 = db
.create_user(
"user3@example.com",
false,
NewUserParams {
github_login: "user3".into(),
github_user_id: 7,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
.await
.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_2, user_2, user_1, false)
.await
.unwrap();
db.invite_channel_member(channel_1_1, user_3, user_1, true)
db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
.await
.unwrap();
@@ -309,27 +281,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
assert_eq!(user_3_invites, &[channel_1_1]);
let members = db
.get_channel_member_details(channel_1_1, user_1)
let mut members = db
.get_channel_participant_details(channel_1_1, user_1)
.await
.unwrap();
members.sort_by_key(|member| member.user_id);
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: false,
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
admin: true,
role: proto::ChannelRole::Admin.into(),
},
]
);
@@ -339,12 +313,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.create_channel("channel_3", Some(channel_1_1), user_1)
.await
.unwrap();
let members = db
.get_channel_member_details(channel_1_3, user_1)
.get_channel_participant_details(channel_1_3, user_1)
.await
.unwrap();
assert_eq!(
@@ -353,12 +327,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
proto::ChannelMember {
user_id: user_1.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
admin: true,
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
admin: false,
role: proto::ChannelRole::Member.into(),
},
]
);
@@ -401,7 +375,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
@@ -409,11 +383,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
let zed_archive_id = zed_id;
let (channel, _) = db
.get_channel(zed_archive_id, user_1)
.await
.unwrap()
.unwrap();
let channel = db.get_channel(zed_archive_id, user_1).await.unwrap();
assert_eq!(channel.name, "zed-archive");
let non_permissioned_rename = db
@@ -446,25 +416,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let gpui2_id = db
.create_channel("gpui2", Some(zed_id), "3", a_id)
.create_channel("gpui2", Some(zed_id), a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
.create_channel("livestreaming", Some(crdb_id), a_id)
.await
.unwrap();
let livestreaming_dag_id = db
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
.await
.unwrap();
@@ -517,12 +484,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
// ========================================================================
// Create a new channel below a channel with multiple parents
let livestreaming_dag_sub_id = db
.create_channel(
"livestreaming_dag_sub",
Some(livestreaming_dag_id),
"6",
a_id,
)
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
.await
.unwrap();
@@ -812,15 +774,15 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap();
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
let projects_id = db
.create_channel("projects", Some(zed_id), "2", user_id)
.create_channel("projects", Some(zed_id), user_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(projects_id), "3", user_id)
.create_channel("livestreaming", Some(projects_id), user_id)
.await
.unwrap();
@@ -849,6 +811,284 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
);
}
test_both_dbs!(
test_user_is_channel_participant,
test_user_is_channel_participant_postgres,
test_user_is_channel_participant_sqlite
);
async fn test_user_is_channel_participant(db: &Arc<Database>) {
let admin = new_test_user(db, "admin@example.com").await;
let member = new_test_user(db, "member@example.com").await;
let guest = new_test_user(db, "guest@example.com").await;
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
let active_channel = db
.create_channel("active", Some(zed_channel), admin)
.await
.unwrap();
let vim_channel = db
.create_channel("vim", Some(active_channel), admin)
.await
.unwrap();
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.invite_channel_member(active_channel, member, admin, ChannelRole::Member)
.await
.unwrap();
db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest)
.await
.unwrap();
db.respond_to_channel_invite(active_channel, member, true)
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(vim_channel, admin, &*tx)
.await
})
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(vim_channel, member, &*tx)
.await
})
.await
.unwrap();
let mut members = db
.get_channel_participant_details(vim_channel, admin)
.await
.unwrap();
members.sort_by_key(|member| member.user_id);
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: admin.to_proto(),
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(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Guest.into(),
},
]
);
db.respond_to_channel_invite(vim_channel, guest, true)
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
.await
})
.await
.unwrap();
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_dag(channels, &[(vim_channel, None)]);
let channels = db.get_channels_for_user(member).await.unwrap().channels;
assert_dag(
channels,
&[(active_channel, None), (vim_channel, Some(active_channel))],
);
db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned)
.await
.unwrap();
assert!(db
.transaction(|tx| async move {
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
.await
})
.await
.is_err());
let mut members = db
.get_channel_participant_details(vim_channel, admin)
.await
.unwrap();
members.sort_by_key(|member| member.user_id);
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: admin.to_proto(),
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(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Banned.into(),
},
]
);
db.remove_channel_member(vim_channel, guest, admin)
.await
.unwrap();
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
.await
.unwrap();
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
.await
.unwrap();
// currently people invited to parent channels are not shown here
let mut members = db
.get_channel_participant_details(vim_channel, admin)
.await
.unwrap();
members.sort_by_key(|member| member.user_id);
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: admin.to_proto(),
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(),
role: proto::ChannelRole::Member.into(),
},
]
);
db.respond_to_channel_invite(zed_channel, guest, true)
.await
.unwrap();
db.transaction(|tx| async move {
db.check_user_is_channel_participant(zed_channel, guest, &*tx)
.await
})
.await
.unwrap();
assert!(db
.transaction(|tx| async move {
db.check_user_is_channel_participant(active_channel, guest, &*tx)
.await
})
.await
.is_err(),);
db.transaction(|tx| async move {
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
.await
})
.await
.unwrap();
let mut members = db
.get_channel_participant_details(vim_channel, admin)
.await
.unwrap();
members.sort_by_key(|member| member.user_id);
assert_eq!(
members,
&[
proto::ChannelMember {
user_id: admin.to_proto(),
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(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: guest.to_proto(),
kind: proto::channel_member::Kind::AncestorMember.into(),
role: proto::ChannelRole::Guest.into(),
},
]
);
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
assert_dag(
channels,
&[(zed_channel, None), (vim_channel, Some(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_channel("active", Some(zed_channel), admin)
.await
.unwrap();
let vim_channel = db
.create_channel("vim", Some(active_channel), admin)
.await
.unwrap();
let vim2_channel = db
.create_channel("vim2", Some(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 { db.most_public_ancestor_for_channel(vim_channel, &*tx).await },
)
.await
.unwrap();
assert_eq!(most_public, Some(zed_channel))
}
#[track_caller]
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
@@ -873,3 +1113,20 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)])
pretty_assertions::assert_eq!(actual_map, expected_map)
}
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
db.create_user(
email,
false,
NewUserParams {
github_login: email[0..email.find("@").unwrap()].to_string(),
github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
invite_count: 0,
},
)
.await
.unwrap()
.user_id
}

View File

@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
.await
.unwrap()
.id,
@@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc<Database>) {
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
db.join_room(
room_id,
user2.user_id,
ConnectionId { owner_id, id: 1 },
"dev",
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
@@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() {
}
}
test_both_dbs!(
test_non_matching_release_channels,
test_non_matching_release_channels_postgres,
test_non_matching_release_channels_sqlite
);
async fn test_non_matching_release_channels(db: &Arc<Database>) {
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user1 = db
.create_user(
&format!("admin@example.com"),
true,
NewUserParams {
github_login: "admin".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let user2 = db
.create_user(
&format!("user@example.com"),
false,
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap();
let room = db
.create_room(
user1.user_id,
ConnectionId { owner_id, id: 0 },
"",
"stable",
)
.await
.unwrap();
db.call(
RoomId::from_proto(room.id),
user1.user_id,
ConnectionId { owner_id, id: 0 },
user2.user_id,
None,
)
.await
.unwrap();
// User attempts to join from preview
let result = db
.join_room(
RoomId::from_proto(room.id),
user2.user_id,
ConnectionId { owner_id, id: 1 },
"preview",
)
.await;
assert!(result.is_err());
// User switches to stable
let result = db
.join_room(
RoomId::from_proto(room.id),
user2.user_id,
ConnectionId { owner_id, id: 1 },
"stable",
)
.await;
assert!(result.is_ok())
}
fn build_background_executor() -> Arc<Background> {
Deterministic::new(0).build_background()
}

View File

@@ -1,10 +1,72 @@
use crate::{
db::{Database, NewUserParams},
db::{ChannelRole, Database, MessageId, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
use time::OffsetDateTime;
test_both_dbs!(
test_channel_message_retrieval,
test_channel_message_retrieval_postgres,
test_channel_message_retrieval_sqlite
);
async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = db
.create_user(
"user@example.com",
false,
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(channel, 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(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
.await
.unwrap()
.0
.to_proto(),
);
}
let messages = db
.get_channel_messages(channel, user, 3, None)
.await
.unwrap()
.into_iter()
.map(|message| message.id)
.collect::<Vec<_>>();
assert_eq!(messages, &all_messages[7..10]);
let messages = db
.get_channel_messages(
channel,
user,
4,
Some(MessageId::from_proto(all_messages[6])),
)
.await
.unwrap()
.into_iter()
.map(|message| message.id)
.collect::<Vec<_>>();
assert_eq!(messages, &all_messages[2..6]);
}
test_both_dbs!(
test_channel_message_nonces,
test_channel_message_nonces_postgres,
@@ -25,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.await
.unwrap()
.user_id;
let channel = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
@@ -92,17 +151,11 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1 = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
let channel_2 = db
.create_channel("channel-2", None, "room", user)
.await
.unwrap();
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
db.invite_channel_member(channel_1, observer, user, false)
db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
.await
.unwrap();
@@ -110,7 +163,7 @@ async fn test_channel_message_new_notification(db: &Arc<Database>) {
.await
.unwrap();
db.invite_channel_member(channel_2, observer, user, false)
db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
.await
.unwrap();

View File

@@ -3,8 +3,8 @@ mod connection_pool;
use crate::{
auth,
db::{
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
ServerId, User, UserId,
self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId,
ProjectId, RoomId, ServerId, User, UserId,
},
executor::Executor,
AppState, Result,
@@ -63,6 +63,7 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
use util::channel::RELEASE_CHANNEL_NAME;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -224,6 +225,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_project_request::<proto::GetCompletions>)
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_project_request::<proto::PrepareRename>)
@@ -253,7 +255,8 @@ impl Server {
.add_request_handler(delete_channel)
.add_request_handler(invite_channel_member)
.add_request_handler(remove_channel_member)
.add_request_handler(set_channel_member_admin)
.add_request_handler(set_channel_member_role)
.add_request_handler(set_channel_visibility)
.add_request_handler(rename_channel)
.add_request_handler(join_channel_buffer)
.add_request_handler(leave_channel_buffer)
@@ -937,11 +940,6 @@ async fn create_room(
util::async_iife!({
let live_kit = live_kit?;
live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()?;
@@ -957,7 +955,12 @@ async fn create_room(
let room = session
.db()
.await
.create_room(session.user_id, session.connection_id, &live_kit_room)
.create_room(
session.user_id,
session.connection_id,
&live_kit_room,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
response.send(proto::CreateRoomResponse {
@@ -975,26 +978,28 @@ async fn join_room(
session: Session,
) -> Result<()> {
let room_id = RoomId::from_proto(request.id);
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
if let Some(channel_id) = channel_id {
return join_channel_internal(channel_id, Box::new(response), session).await;
}
let joined_room = {
let room = session
.db()
.await
.join_room(room_id, session.user_id, session.connection_id)
.join_room(
room_id,
session.user_id,
session.connection_id,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
room_updated(&room.room, &session.peer);
room.into_inner()
};
if let Some(channel_id) = joined_room.channel_id {
channel_updated(
channel_id,
&joined_room.room,
&joined_room.channel_members,
&session.peer,
&*session.connection_pool().await,
)
}
for connection_id in session
.connection_pool()
.await
@@ -1032,7 +1037,7 @@ async fn join_room(
response.send(proto::JoinRoomResponse {
room: Some(joined_room.room),
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
channel_id: None,
live_kit_connection_info,
})?;
@@ -2195,20 +2200,16 @@ async fn create_channel(
session: Session,
) -> Result<()> {
let db = session.db().await;
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit.create_room(live_kit_room.clone()).await?;
}
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let id = db
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
.create_channel(&request.name, parent_id, session.user_id)
.await?;
let channel = proto::Channel {
id: id.to_proto(),
name: request.name,
visibility: proto::ChannelVisibility::Members as i32,
};
response.send(proto::CreateChannelResponse {
@@ -2281,17 +2282,20 @@ async fn invite_channel_member(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let invitee_id = UserId::from_proto(request.user_id);
db.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
.await?;
db.invite_channel_member(
channel_id,
invitee_id,
session.user_id,
request.role().into(),
)
.await?;
let (channel, _) = db
.get_channel(channel_id, session.user_id)
.await?
.ok_or_else(|| anyhow!("channel not found"))?;
let channel = db.get_channel(channel_id, session.user_id).await?;
let mut update = proto::UpdateChannels::default();
update.channel_invitations.push(proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
});
for connection_id in session
@@ -2333,27 +2337,63 @@ async fn remove_channel_member(
Ok(())
}
async fn set_channel_member_admin(
request: proto::SetChannelMemberAdmin,
response: Response<proto::SetChannelMemberAdmin>,
async fn set_channel_visibility(
request: proto::SetChannelVisibility,
response: Response<proto::SetChannelVisibility>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let visibility = request.visibility().into();
let channel = db
.set_channel_visibility(channel_id, visibility, session.user_id)
.await?;
let mut update = proto::UpdateChannels::default();
update.channels.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
visibility: channel.visibility.into(),
});
let member_ids = db.get_channel_members(channel_id).await?;
let connection_pool = session.connection_pool().await;
for member_id in member_ids {
for connection_id in connection_pool.user_connection_ids(member_id) {
session.peer.send(connection_id, update.clone())?;
}
}
response.send(proto::Ack {})?;
Ok(())
}
async fn set_channel_member_role(
request: proto::SetChannelMemberRole,
response: Response<proto::SetChannelMemberRole>,
session: Session,
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let member_id = UserId::from_proto(request.user_id);
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
let channel_member = db
.set_channel_member_role(
channel_id,
session.user_id,
member_id,
request.role().into(),
)
.await?;
let (channel, has_accepted) = db
.get_channel(channel_id, member_id)
.await?
.ok_or_else(|| anyhow!("channel not found"))?;
let channel = db.get_channel(channel_id, session.user_id).await?;
let mut update = proto::UpdateChannels::default();
if has_accepted {
if channel_member.accepted {
update.channel_permissions.push(proto::ChannelPermission {
channel_id: channel.id.to_proto(),
is_admin: request.admin,
role: request.role,
});
}
@@ -2376,13 +2416,14 @@ async fn rename_channel(
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let new_name = db
let channel = db
.rename_channel(channel_id, session.user_id, &request.name)
.await?;
let channel = proto::Channel {
id: request.channel_id,
name: new_name,
id: channel.id.to_proto(),
name: channel.name,
visibility: channel.visibility.into(),
};
response.send(proto::RenameChannelResponse {
channel: Some(channel.clone()),
@@ -2420,6 +2461,7 @@ async fn link_channel(
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
})
.collect(),
@@ -2511,6 +2553,7 @@ async fn move_channel(
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
})
.collect(),
@@ -2536,7 +2579,7 @@ async fn get_channel_members(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let members = db
.get_channel_member_details(channel_id, session.user_id)
.get_channel_participant_details(channel_id, session.user_id)
.await?;
response.send(proto::GetChannelMembersResponse { members })?;
Ok(())
@@ -2552,53 +2595,68 @@ async fn respond_to_channel_invite(
db.respond_to_channel_invite(channel_id, session.user_id, request.accept)
.await?;
if request.accept {
channel_membership_updated(db, channel_id, &session).await?;
} else {
let mut update = proto::UpdateChannels::default();
update
.remove_channel_invitations
.push(channel_id.to_proto());
session.peer.send(session.connection_id, update)?;
}
response.send(proto::Ack {})?;
Ok(())
}
async fn channel_membership_updated(
db: tokio::sync::MutexGuard<'_, DbHandle>,
channel_id: ChannelId,
session: &Session,
) -> Result<(), crate::Error> {
let mut update = proto::UpdateChannels::default();
update
.remove_channel_invitations
.push(channel_id.to_proto());
if request.accept {
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
update
.channels
.extend(
result
.channels
.channels
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
}),
);
update.unseen_channel_messages = result.channel_messages;
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
update.insert_edge = result.channels.edges;
update
.channel_participants
.extend(
result
.channel_participants
.into_iter()
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
}),
);
update
.channel_permissions
.extend(
result
.channels_with_admin_privileges
.into_iter()
.map(|channel_id| proto::ChannelPermission {
channel_id: channel_id.to_proto(),
is_admin: true,
}),
);
}
session.peer.send(session.connection_id, update)?;
response.send(proto::Ack {})?;
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
update.channels.extend(
result
.channels
.channels
.into_iter()
.map(|channel| proto::Channel {
id: channel.id.to_proto(),
visibility: channel.visibility.into(),
name: channel.name,
}),
);
update.unseen_channel_messages = result.channel_messages;
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
update.insert_edge = result.channels.edges;
update
.channel_participants
.extend(
result
.channel_participants
.into_iter()
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
channel_id: channel_id.to_proto(),
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
}),
);
update
.channel_permissions
.extend(
result
.channels_with_admin_privileges
.into_iter()
.map(|channel_id| proto::ChannelPermission {
channel_id: channel_id.to_proto(),
role: proto::ChannelRole::Admin.into(),
}),
);
session.peer.send(session.connection_id, update)?;
Ok(())
}
@@ -2608,15 +2666,39 @@ async fn join_channel(
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
join_channel_internal(channel_id, Box::new(response), session).await
}
trait JoinChannelInternalResponse {
fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
}
impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
Response::<proto::JoinChannel>::send(self, result)
}
}
impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
Response::<proto::JoinRoom>::send(self, result)
}
}
async fn join_channel_internal(
channel_id: ChannelId,
response: Box<impl JoinChannelInternalResponse>,
session: Session,
) -> Result<()> {
let joined_room = {
leave_room_for_session(&session).await?;
let db = session.db().await;
let room_id = db.room_id_for_channel(channel_id).await?;
let joined_room = db
.join_room(room_id, session.user_id, session.connection_id)
let (joined_room, joined_channel) = db
.join_channel(
channel_id,
session.user_id,
session.connection_id,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
@@ -2639,9 +2721,13 @@ async fn join_channel(
live_kit_connection_info,
})?;
if let Some(joined_channel) = joined_channel {
channel_membership_updated(db, joined_channel, &session).await?
}
room_updated(&joined_room.room, &session.peer);
joined_room.into_inner()
joined_room
};
channel_updated(
@@ -2653,7 +2739,6 @@ async fn join_channel(
);
update_user_contacts(session.user_id, &session).await?;
Ok(())
}
@@ -3063,6 +3148,7 @@ fn build_initial_channels_update(
update.channels.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
visibility: channel.visibility.into(),
});
}
@@ -3087,7 +3173,7 @@ fn build_initial_channels_update(
.into_iter()
.map(|id| proto::ChannelPermission {
channel_id: id.to_proto(),
is_admin: true,
role: proto::ChannelRole::Admin.into(),
}),
);
@@ -3095,6 +3181,8 @@ fn build_initial_channels_update(
update.channel_invitations.push(proto::Channel {
id: channel.id.to_proto(),
name: channel.name,
// TODO: Visibility
visibility: ChannelVisibility::Public.into(),
});
}

View File

@@ -11,7 +11,10 @@ use collections::HashMap;
use editor::{Anchor, Editor, ToOffset};
use futures::future;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use rpc::{
proto::{self, PeerId},
RECEIVE_TIMEOUT,
};
use serde_json::json;
use std::{ops::Range, sync::Arc};
@@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
Channel {
id,
name: name.to_string(),
visibility: proto::ChannelVisibility::Members,
unseen_note_version: None,
unseen_message_id: None,
}

View File

@@ -6,7 +6,10 @@ use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::User;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use rpc::{proto, RECEIVE_TIMEOUT};
use rpc::{
proto::{self, ChannelRole},
RECEIVE_TIMEOUT,
};
use std::sync::Arc;
#[gpui::test]
@@ -68,7 +71,12 @@ async fn test_core_channels(
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
let invite = store.invite_member(
channel_a_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
);
// Make sure we're synchronously storing the pending invite
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
@@ -103,12 +111,12 @@ async fn test_core_channels(
&[
(
client_a.user_id().unwrap(),
true,
proto::ChannelRole::Admin,
proto::channel_member::Kind::Member,
),
(
client_b.user_id().unwrap(),
false,
proto::ChannelRole::Member,
proto::channel_member::Kind::Invitee,
),
],
@@ -183,7 +191,12 @@ async fn test_core_channels(
client_a
.channel_store()
.update(cx_a, |store, cx| {
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
store.set_member_role(
channel_a_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Admin,
cx,
)
})
.await
.unwrap();
@@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
#[track_caller]
fn assert_members_eq(
members: &[ChannelMembership],
expected_members: &[(u64, bool, proto::channel_member::Kind)],
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
) {
assert_eq!(
members
.iter()
.map(|member| (member.user.id, member.admin, member.kind))
.map(|member| (member.user.id, member.role, member.kind))
.collect::<Vec<_>>(),
expected_members
);
@@ -380,6 +393,8 @@ async fn test_channel_room(
// Give everyone a chance to observe user A joining
deterministic.run_until_parked();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
@@ -609,7 +624,12 @@ async fn test_permissions_update_while_invited(
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
channel_store.invite_member(
rust_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
.await
.unwrap();
@@ -632,7 +652,12 @@ async fn test_permissions_update_while_invited(
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
channel_store.set_member_role(
rust_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Admin,
cx,
)
})
.await
.unwrap();
@@ -801,7 +826,12 @@ async fn test_lost_channel_creation(
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
channel_store.invite_member(
channel_id,
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
cx,
)
})
.await
.unwrap();
@@ -882,6 +912,119 @@ async fn test_lost_channel_creation(
],
);
}
#[gpui::test]
async fn test_guest_access(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channels = server
.make_channel_tree(&[("channel-a", None)], (&client_a, cx_a))
.await;
let channel_a_id = channels[0];
let active_call_b = cx_b.read(ActiveCall::global);
// should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
.await
.is_err());
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx)
})
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
assert!(client_b
.channel_store()
.update(cx_b, |channel_store, _| channel_store
.channel_for_id(channel_a_id)
.is_some()));
client_a.channel_store().update(cx_a, |channel_store, _| {
let participants = channel_store.channel_participants(channel_a_id);
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap());
})
}
#[gpui::test]
async fn test_invite_access(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channels = server
.make_channel_tree(
&[("channel-a", None), ("channel-b", Some("channel-a"))],
(&client_a, cx_a),
)
.await;
let channel_a_id = channels[0];
let channel_b_id = channels[0];
let active_call_b = cx_b.read(ActiveCall::global);
// should not be allowed to join
assert!(active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.is_err());
client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
channel_store.invite_member(
channel_a_id,
client_b.user_id().unwrap(),
ChannelRole::Member,
cx,
)
})
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.unwrap();
deterministic.run_until_parked();
client_b.channel_store().update(cx_b, |channel_store, _| {
assert!(channel_store.channel_for_id(channel_b_id).is_some());
assert!(channel_store.channel_for_id(channel_a_id).is_some());
});
client_a.channel_store().update(cx_a, |channel_store, _| {
let participants = channel_store.channel_participants(channel_b_id);
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].id, client_b.user_id().unwrap());
})
}
#[gpui::test]
async fn test_channel_moving(

View File

@@ -184,20 +184,12 @@ async fn test_basic_following(
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
"followers seen by {name}"
);
}
// Client C unfollows client A.
@@ -207,46 +199,39 @@ async fn test_basic_following(
// All clients see that clients B is following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b])],
"followers seen by {name}"
);
}
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.follow(peer_id_a, cx);
});
workspace_c
.update(cx_c, |workspace, cx| {
workspace.follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b, peer_id_c])],
"followers seen by {name}"
);
}
// Client D follows client C.
// Client D follows client B, then switches to following client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_b, cx).unwrap()
})
.await
.unwrap();
workspace_d
.update(cx_d, |workspace, cx| {
workspace.follow(peer_id_c, cx).unwrap()
@@ -256,20 +241,15 @@ async fn test_basic_following(
// All clients see that D is following C
cx_d.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[peer_id_d],
"checking followers for C as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[
(peer_id_a, vec![peer_id_b, peer_id_c]),
(peer_id_c, vec![peer_id_d])
],
"followers seen by {name}"
);
}
// Client C closes the project.
@@ -278,32 +258,12 @@ async fn test_basic_following(
// Clients A and B see that client B is following A, and client C is not present in the followers.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
}
// All clients see that no-one is following C
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[],
"checking followers for C as {name}"
);
});
for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
assert_eq!(
followers_by_leader(project_id, cx),
&[(peer_id_a, vec![peer_id_b]),],
"followers seen by {name}"
);
}
// When client A activates a different editor, client B does so as well.
@@ -1667,6 +1627,30 @@ struct PaneSummary {
items: Vec<(bool, String)>,
}
fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
cx.read(|cx| {
let active_call = ActiveCall::global(cx).read(cx);
let peer_id = active_call.client().peer_id();
let room = active_call.room().unwrap().read(cx);
let mut result = room
.remote_participants()
.values()
.map(|participant| participant.peer_id)
.chain(peer_id)
.filter_map(|peer_id| {
let followers = room.followers_for(peer_id, project_id);
if followers.is_empty() {
None
} else {
Some((peer_id, followers.to_vec()))
}
})
.collect::<Vec<_>>();
result.sort_by_key(|e| e.0);
result
})
}
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
workspace.read_with(cx, |workspace, cx| {
let active_pane = workspace.active_pane();

View File

@@ -20,7 +20,9 @@ use language::{
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
use project::{
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
};
use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
@@ -4407,8 +4409,6 @@ async fn test_formatting_buffer(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
use project::FormatTrigger;
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -4511,6 +4511,132 @@ async fn test_formatting_buffer(
);
}
#[gpui::test(iterations = 10)]
async fn test_prettier_formatting_buffer(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let test_plugin = "test_plugin";
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
}))
.await;
let language = Arc::new(language);
client_a.language_registry().add(Arc::clone(&language));
// Here we insert a fake tree with a directory that exists on disk. This is needed
// because later we'll invoke a command, which requires passing a working directory
// that points to a valid location on disk.
let directory = env::current_dir().unwrap();
let buffer_text = "let one = \"two\"";
client_a
.fs()
.insert_tree(&directory, json!({ "a.rs": buffer_text }))
.await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let prettier_format_suffix = project_a.update(cx_a, |project, _| {
let suffix = project.enable_test_prettier(&[test_plugin]);
project.languages().add(language);
suffix
});
let buffer_a = cx_a
.background()
.spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let buffer_b = cx_b
.background()
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
.await
.unwrap();
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::Auto);
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::LanguageServer);
});
});
});
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
panic!(
"Unexpected: prettier should be preferred since it's enabled and language supports it"
)
});
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
cx,
)
})
.await
.unwrap();
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after client's request"
);
project_a
.update(cx_a, |project, cx| {
project.format(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
cx,
)
})
.await
.unwrap();
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after host's request"
);
}
#[gpui::test(iterations = 10)]
async fn test_definition(
deterministic: Arc<Deterministic>,

View File

@@ -1,3 +1,5 @@
use crate::db::ChannelRole;
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use anyhow::Result;
use async_trait::async_trait;
@@ -46,16 +48,11 @@ impl RandomizedTest for RandomChannelBufferTest {
let db = &server.app_state.db;
for ix in 0..CHANNEL_COUNT {
let id = db
.create_channel(
&format!("channel-{ix}"),
None,
&format!("livekit-room-{ix}"),
users[0].user_id,
)
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
.await
.unwrap();
for user in &users[1..] {
db.invite_channel_member(id, user.user_id, users[0].user_id, false)
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
.await
.unwrap();
db.respond_to_channel_invite(id, user.user_id, true)

View File

@@ -15,9 +15,10 @@ use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use rpc::RECEIVE_TIMEOUT;
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
use settings::SettingsStore;
use std::{
cell::{Ref, RefCell, RefMut},
@@ -44,6 +45,7 @@ pub struct TestServer {
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
channel_store: ModelHandle<ChannelStore>,
state: RefCell<TestClientState>,
}
@@ -206,20 +208,18 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
channel_store: channel_store.clone(),
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
@@ -231,7 +231,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
channel::init(&client);
channel::init(&client, user_store, cx);
});
client
@@ -242,6 +242,7 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
channel_store: cx.read(ChannelStore::global).clone(),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -310,10 +311,9 @@ impl TestServer {
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
let (admin_client, admin_cx) = admin;
let channel_id = admin_client
.app_state
.channel_store
let (_, admin_cx) = admin;
let channel_id = admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
@@ -321,14 +321,13 @@ impl TestServer {
.unwrap();
for (member_client, member_cx) in members {
admin_client
.app_state
.channel_store
admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
member_client.user_id().unwrap(),
false,
ChannelRole::Member,
cx,
)
})
@@ -337,9 +336,8 @@ impl TestServer {
admin_cx.foreground().run_until_parked();
member_client
.app_state
.channel_store
member_cx
.read(ChannelStore::global)
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
@@ -447,7 +445,7 @@ impl TestClient {
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
&self.app_state.channel_store
&self.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -571,6 +569,7 @@ impl TestClient {
cx.update(|cx| {
Project::local(
self.client().clone(),
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
@@ -614,21 +613,25 @@ impl TestClient {
) {
let (other_client, other_cx) = user;
self.app_state
.channel_store
cx_self
.read(ChannelStore::global)
.update(cx_self, |channel_store, cx| {
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
channel_store.invite_member(
channel,
other_client.user_id().unwrap(),
ChannelRole::Admin,
cx,
)
})
.await
.unwrap();
cx_self.foreground().run_until_parked();
other_client
.app_state
.channel_store
.update(other_cx, |channels, _| {
channels.respond_to_channel_invite(channel, true)
other_cx
.read(ChannelStore::global)
.update(other_cx, |channel_store, _| {
channel_store.respond_to_channel_invite(channel, true)
})
.await
.unwrap();

View File

@@ -24,7 +24,7 @@ use workspace::{
item::{FollowableItem, Item, ItemHandle},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
};
actions!(channel_view, [Deploy]);
@@ -73,7 +73,7 @@ impl ChannelView {
) -> Task<Result<ViewHandle<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
@@ -93,15 +93,36 @@ impl ChannelView {
}
pane.update(&mut cx, |pane, cx| {
pane.items_of_type::<Self>()
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
.unwrap_or_else(|| {
cx.add_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
})
})
let buffer_id = channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
return existing_view;
}
}
let view = cx.add_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
}
view
})
.ok_or_else(|| anyhow!("pane was dropped"))
})
@@ -285,10 +306,14 @@ impl FollowableItem for ChannelView {
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let channel = self.channel_buffer.read(cx).channel();
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
}
Some(proto::view::Variant::ChannelView(
proto::view::ChannelView {
channel_id: channel.id,
channel_id: channel_buffer.channel().id,
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
{

View File

@@ -81,7 +81,7 @@ impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
@@ -355,8 +355,12 @@ impl ChatPanel {
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let (message, is_continuation, is_last) = {
let (message, is_continuation, is_last, is_admin) = {
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
let is_admin = self
.channel_store
.read(cx)
.is_user_admin(active_chat.channel().id);
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix);
let is_continuation = last_message.id != this_message.id
@@ -366,6 +370,7 @@ impl ChatPanel {
active_chat.message(ix).clone(),
is_continuation,
active_chat.message_count() == ix + 1,
is_admin,
)
};
@@ -386,12 +391,13 @@ impl ChatPanel {
};
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove =
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
Some(id)
} else {
None
};
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin)
{
Some(id)
} else {
None
};
enum MessageBackgroundHighlight {}
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {

View File

@@ -11,7 +11,10 @@ use anyhow::Result;
use call::ActiveCall;
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
use channel_modal::ChannelModal;
use client::{proto::PeerId, Client, Contact, User, UserStore};
use client::{
proto::{self, PeerId},
Client, Contact, User, UserStore,
};
use contact_finder::ContactFinder;
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
@@ -34,8 +37,8 @@ use gpui::{
},
impl_actions,
platform::{CursorStyle, MouseButton, PromptLevel},
serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{Fs, Project};
@@ -100,6 +103,11 @@ pub struct JoinChannelChat {
pub channel_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CopyChannelLink {
pub channel_id: u64,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct StartMoveChannelFor {
channel_id: ChannelId,
@@ -157,6 +165,7 @@ impl_actions!(
OpenChannelNotes,
JoinChannelCall,
JoinChannelChat,
CopyChannelLink,
LinkChannel,
StartMoveChannelFor,
StartLinkChannelFor,
@@ -205,6 +214,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabPanel::expand_selected_channel);
cx.add_action(CollabPanel::open_channel_notes);
cx.add_action(CollabPanel::join_channel_chat);
cx.add_action(CollabPanel::copy_channel_link);
cx.add_action(
|panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
@@ -421,7 +431,7 @@ enum ListEntry {
is_last: bool,
},
ParticipantScreen {
peer_id: PeerId,
peer_id: Option<PeerId>,
is_last: bool,
},
IncomingRequest(Arc<User>),
@@ -435,6 +445,9 @@ enum ListEntry {
ChannelNotes {
channel_id: ChannelId,
},
ChannelChat {
channel_id: ChannelId,
},
ChannelEditor {
depth: usize,
},
@@ -595,6 +608,13 @@ impl CollabPanel {
ix,
cx,
),
ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
*channel_id,
&theme.collab_panel,
is_selected,
ix,
cx,
),
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
channel.clone(),
this.channel_store.clone(),
@@ -648,7 +668,7 @@ impl CollabPanel {
channel_editing_state: None,
selection: None,
user_store: workspace.user_store().clone(),
channel_store: workspace.app_state().channel_store.clone(),
channel_store: ChannelStore::global(cx),
project: workspace.project().clone(),
subscriptions: Vec::default(),
match_candidates: Vec::default(),
@@ -797,7 +817,8 @@ impl CollabPanel {
let room = room.read(cx);
if let Some(channel_id) = room.channel_id() {
self.entries.push(ListEntry::ChannelNotes { channel_id })
self.entries.push(ListEntry::ChannelNotes { channel_id });
self.entries.push(ListEntry::ChannelChat { channel_id })
}
// Populate the active user.
@@ -829,7 +850,13 @@ impl CollabPanel {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: user_id,
is_last: projects.peek().is_none(),
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
});
}
if room.is_screen_sharing() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: None,
is_last: true,
});
}
}
@@ -873,7 +900,7 @@ impl CollabPanel {
}
if !participant.video_tracks.is_empty() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: participant.peer_id,
peer_id: Some(participant.peer_id),
is_last: true,
});
}
@@ -1218,14 +1245,18 @@ impl CollabPanel {
) -> AnyElement<Self> {
enum CallParticipant {}
enum CallParticipantTooltip {}
enum LeaveCallButton {}
enum LeaveCallTooltip {}
let collab_theme = &theme.collab_panel;
let is_current_user =
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
let content =
MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
let content = MouseEventHandler::new::<CallParticipant, _>(
user.id as usize,
cx,
|mouse_state, cx| {
let style = if is_current_user {
*collab_theme
.contact_row
@@ -1261,14 +1292,32 @@ impl CollabPanel {
Label::new("Calling", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(collab_theme.calling_indicator.container)
.aligned(),
.aligned()
.into_any(),
)
} else if is_current_user {
Some(
Label::new("You", collab_theme.calling_indicator.text.clone())
.contained()
.with_style(collab_theme.calling_indicator.container)
.aligned(),
MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
render_icon_button(
theme
.collab_panel
.leave_call_button
.style_for(is_selected, state),
"icons/exit.svg",
)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
Self::leave_call(cx);
})
.with_tooltip::<LeaveCallTooltip>(
0,
"Leave call",
None,
theme.tooltip.clone(),
cx,
)
.into_any(),
)
} else {
None
@@ -1277,7 +1326,8 @@ impl CollabPanel {
.with_height(collab_theme.row_height)
.contained()
.with_style(style)
});
},
);
if is_current_user || is_pending || peer_id.is_none() {
return content.into_any();
@@ -1399,7 +1449,7 @@ impl CollabPanel {
}
fn render_participant_screen(
peer_id: PeerId,
peer_id: Option<PeerId>,
is_last: bool,
is_selected: bool,
theme: &theme::CollabPanel,
@@ -1414,8 +1464,8 @@ impl CollabPanel {
.unwrap_or(0.);
let tree_branch = theme.tree_branch;
MouseEventHandler::new::<OpenSharedScreen, _>(
peer_id.as_u64() as usize,
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
cx,
|mouse_state, cx| {
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
@@ -1453,16 +1503,20 @@ impl CollabPanel {
.contained()
.with_style(row.container)
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(peer_id, cx)
});
}
})
.into_any()
);
if peer_id.is_none() {
return handler.into_any();
}
handler
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(peer_id.unwrap(), cx)
});
}
})
.into_any()
}
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -1489,23 +1543,32 @@ impl CollabPanel {
enum AddChannel {}
let tooltip_style = &theme.tooltip;
let mut channel_link = None;
let mut channel_tooltip_text = None;
let mut channel_icon = None;
let text = match section {
Section::ActiveCall => {
let channel_name = iife!({
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
let name = self
.channel_store
.read(cx)
.channel_for_id(channel_id)?
.name
.as_str();
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
Some(name)
channel_link = Some(channel.link());
(channel_icon, channel_tooltip_text) = match channel.visibility {
proto::ChannelVisibility::Public => {
(Some("icons/public.svg"), Some("Copy public channel link."))
}
proto::ChannelVisibility::Members => {
(Some("icons/hash.svg"), Some("Copy private channel link."))
}
};
Some(channel.name.as_str())
});
if let Some(name) = channel_name {
Cow::Owned(format!("#{}", name))
Cow::Owned(format!("{}", name))
} else {
Cow::Borrowed("Current Call")
}
@@ -1520,28 +1583,30 @@ impl CollabPanel {
enum AddContact {}
let button = match section {
Section::ActiveCall => Some(
Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link.clone();
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
render_icon_button(
theme
.collab_panel
.leave_call_button
.style_for(is_selected, state),
"icons/exit.svg",
"icons/link.svg",
)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
Self::leave_call(cx);
.on_click(MouseButton::Left, move |_, _, cx| {
let item = ClipboardItem::new(channel_link_copy.clone());
cx.write_to_clipboard(item)
})
.with_tooltip::<AddContact>(
0,
"Leave call",
channel_tooltip_text.unwrap(),
None,
tooltip_style.clone(),
cx,
),
),
)
}),
Section::Contacts => Some(
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
render_icon_button(
@@ -1626,6 +1691,21 @@ impl CollabPanel {
theme.collab_panel.contact_username.container.margin.left,
),
)
} else if let Some(channel_icon) = channel_icon {
Some(
Svg::new(channel_icon)
.with_color(header_style.text.color)
.constrained()
.with_max_width(icon_size)
.with_max_height(icon_size)
.aligned()
.constrained()
.with_width(icon_size)
.contained()
.with_margin_right(
theme.collab_panel.contact_username.container.margin.left,
),
)
} else {
None
})
@@ -1901,6 +1981,12 @@ impl CollabPanel {
let channel_id = channel.id;
let collab_theme = &theme.collab_panel;
let has_children = self.channel_store.read(cx).has_children(channel_id);
let is_public = self
.channel_store
.read(cx)
.channel_for_id(channel_id)
.map(|channel| channel.visibility)
== Some(proto::ChannelVisibility::Public);
let other_selected =
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
@@ -1958,12 +2044,16 @@ impl CollabPanel {
Flex::<Self>::row()
.with_child(
Svg::new("icons/hash.svg")
.with_color(collab_theme.channel_hash.color)
.constrained()
.with_width(collab_theme.channel_hash.width)
.aligned()
.left(),
Svg::new(if is_public {
"icons/public.svg"
} else {
"icons/hash.svg"
})
.with_color(collab_theme.channel_hash.color)
.constrained()
.with_width(collab_theme.channel_hash.width)
.aligned()
.left(),
)
.with_child({
let style = collab_theme.channel_name.inactive_state();
@@ -2268,7 +2358,7 @@ impl CollabPanel {
.with_child(render_tree_branch(
tree_branch,
&row.name.text,
true,
false,
vec2f(host_avatar_width, theme.row_height),
cx.font_cache(),
))
@@ -2301,6 +2391,62 @@ impl CollabPanel {
.into_any()
}
fn render_channel_chat(
&self,
channel_id: ChannelId,
theme: &theme::CollabPanel,
is_selected: bool,
ix: usize,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum ChannelChat {}
let host_avatar_width = theme
.contact_avatar
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
let row = theme.project_row.in_state(is_selected).style_for(state);
Flex::<Self>::row()
.with_child(render_tree_branch(
tree_branch,
&row.name.text,
true,
vec2f(host_avatar_width, theme.row_height),
cx.font_cache(),
))
.with_child(
Svg::new("icons/conversations.svg")
.with_color(theme.channel_hash.color)
.constrained()
.with_width(theme.channel_hash.width)
.aligned()
.left(),
)
.with_child(
Label::new("chat", theme.channel_name.text.clone())
.contained()
.with_style(theme.channel_name.container)
.aligned()
.left()
.flex(1., true),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(*theme.channel_row.style_for(is_selected, state))
.with_padding_left(theme.channel_row.default_style().padding.left)
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.into_any()
}
fn render_channel_invite(
channel: Arc<Channel>,
channel_store: ModelHandle<ChannelStore>,
@@ -2568,6 +2714,13 @@ impl CollabPanel {
},
));
items.push(ContextMenuItem::action(
"Copy Channel Link",
CopyChannelLink {
channel_id: path.channel_id(),
},
));
if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
let parent_id = path.parent_id();
@@ -2757,6 +2910,9 @@ impl CollabPanel {
}
}
ListEntry::ParticipantScreen { peer_id, .. } => {
let Some(peer_id) = peer_id else {
return;
};
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(*peer_id, cx)
@@ -3187,49 +3343,19 @@ impl CollabPanel {
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
let window = cx.window();
let active_call = ActiveCall::global(cx);
cx.spawn(|_, mut cx| async move {
if active_call.read_with(&mut cx, |active_call, cx| {
if let Some(room) = active_call.room() {
let room = room.read(cx);
room.is_sharing_project() && room.remote_participants().len() > 0
} else {
false
}
}) {
let answer = window.prompt(
PromptLevel::Warning,
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
&["Yes, Join Channel", "Cancel"],
&mut cx,
);
if let Some(mut answer) = answer {
if answer.next().await == Some(1) {
return anyhow::Ok(());
}
}
}
let room = active_call
.update(&mut cx, |call, cx| call.join_channel(channel_id, cx))
.await?;
let task = room.update(&mut cx, |room, cx| {
let workspace = workspace.upgrade(cx)?;
let (project, host) = room.most_active_project()?;
let app_state = workspace.read(cx).app_state().clone();
Some(workspace::join_remote_project(project, host, app_state, cx))
});
if let Some(task) = task {
task.await?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
let Some(workspace) = self.workspace.upgrade(cx) else {
return;
};
let Some(handle) = cx.window().downcast::<Workspace>() else {
return;
};
workspace::join_channel(
channel_id,
workspace.read(cx).app_state().clone(),
Some(handle),
cx,
)
.detach_and_log_err(cx)
}
fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext<Self>) {
@@ -3246,6 +3372,15 @@ impl CollabPanel {
});
}
}
fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.read(cx);
let Some(channel) = channel_store.channel_for_id(action.channel_id) else {
return;
};
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item)
}
}
fn render_tree_branch(
@@ -3505,6 +3640,14 @@ impl PartialEq for ListEntry {
return channel_id == other_id;
}
}
ListEntry::ChannelChat { channel_id } => {
if let ListEntry::ChannelChat {
channel_id: other_id,
} = other
{
return channel_id == other_id;
}
}
ListEntry::ChannelInvite(channel_1) => {
if let ListEntry::ChannelInvite(channel_2) = other {
return channel_1.id == channel_2.id;

View File

@@ -1,12 +1,16 @@
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::{proto, User, UserId, UserStore};
use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions,
elements::*,
platform::{CursorStyle, MouseButton},
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
ViewHandle,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc;
@@ -96,11 +100,14 @@ impl ChannelModal {
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let members = channel_store
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate_mut().members = members);
@@ -182,6 +189,81 @@ impl View for ChannelModal {
.into_any()
}
fn render_visibility(
channel_id: ChannelId,
visibility: ChannelVisibility,
theme: &theme::TabbedModal,
cx: &mut ViewContext<ChannelModal>,
) -> AnyElement<ChannelModal> {
enum TogglePublic {}
if visibility == ChannelVisibility::Members {
return Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Public,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any();
}
Flex::row()
.with_child(
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
let style = theme.visibility_toggle.style_for(state);
Label::new(format!("{}", "Public access: ON"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.channel_store
.update(cx, |channel_store, cx| {
channel_store.set_channel_visibility(
channel_id,
ChannelVisibility::Members,
cx,
)
})
.detach_and_log_err(cx);
})
.with_cursor_style(CursorStyle::PointingHand),
)
.with_spacing(14.0)
.with_child(
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
let style = theme.channel_link.style_for(state);
Label::new(format!("{}", "copy link"), style.text.clone())
.contained()
.with_style(style.container.clone())
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(channel) =
this.channel_store.read(cx).channel_for_id(channel_id)
{
let item = ClipboardItem::new(channel.link());
cx.write_to_clipboard(item);
}
})
.with_cursor_style(CursorStyle::PointingHand),
)
.into_any()
}
Flex::column()
.with_child(
Flex::column()
@@ -190,6 +272,7 @@ impl View for ChannelModal {
.contained()
.with_style(theme.title.container.clone()),
)
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
.with_child(Flex::row().with_children([
render_mode_button::<InviteMembers>(
Mode::InviteMembers,
@@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
match self.mode {
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
Mode::ManageMembers => {
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
}
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_selected_member(cx);
@@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
let full_theme = &theme::current(cx);
let theme = &full_theme.collab_panel.channel_modal;
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
let (user, admin) = self.user_at_index(ix).unwrap();
let (user, role) = self.user_at_index(ix).unwrap();
let request_status = self.member_status(user.id, cx);
let style = tabbed_modal
@@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
},
)
})
.with_children(admin.and_then(|admin| {
(in_manage && admin).then(|| {
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
Some(
Label::new("Admin", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left()
})
}))
.left(),
)
} else if in_manage && role == Some(ChannelRole::Guest) {
Some(
Label::new("Guest", theme.member_tag.text.clone())
.contained()
.with_style(theme.member_tag.container)
.aligned()
.left(),
)
} else {
None
})
.with_children({
let svg = match self.mode {
Mode::ManageMembers => Some(
@@ -502,13 +597,13 @@ impl ChannelModalDelegate {
})
}
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
match self.mode {
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
let channel_membership = self.members.get(*ix)?;
Some((
channel_membership.user.clone(),
Some(channel_membership.admin),
Some(channel_membership.role),
))
}),
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
@@ -516,17 +611,21 @@ impl ChannelModalDelegate {
}
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let (user, admin) = self.user_at_index(self.selected_index)?;
let admin = !admin.unwrap_or(false);
let (user, role) = self.user_at_index(self.selected_index)?;
let new_role = if role == Some(ChannelRole::Admin) {
ChannelRole::Member
} else {
ChannelRole::Admin
};
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_admin(self.channel_id, user.id, admin, cx)
store.set_member_role(self.channel_id, user.id, new_role, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
let this = picker.delegate_mut();
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.admin = admin;
member.role = new_role;
}
cx.focus_self();
cx.notify();
@@ -572,25 +671,30 @@ impl ChannelModalDelegate {
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, false, cx)
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
this.delegate_mut().members.push(ChannelMembership {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
admin: false,
});
role: ChannelRole::Member,
};
let members = &mut this.delegate_mut().members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
})
})
.detach_and_log_err(cx);
}
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
self.context_menu.update(cx, |context_menu, cx| {
context_menu.show(
Default::default(),
@@ -598,7 +702,7 @@ impl ChannelModalDelegate {
vec![
ContextMenuItem::action("Remove", RemoveMember),
ContextMenuItem::action(
if user_is_admin {
if role == ChannelRole::Admin {
"Make non-admin"
} else {
"Make admin"

View File

@@ -2,6 +2,7 @@ use crate::{
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
};
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
use clock::ReplicaId;
@@ -1177,22 +1178,38 @@ impl CollabTitlebarItem {
.with_style(theme.titlebar.offline_icon.container)
.into_any(),
),
client::Status::UpgradeRequired => Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
auto_update::check(&Default::default(), cx);
})
.into_any(),
),
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Installing)
| Some(AutoUpdateStatus::Downloading)
| Some(AutoUpdateStatus::Checking) => "Updating...",
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
"Please update Zed to Collaborate"
}
};
Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(label, theme.titlebar.outdated_warning.text.clone())
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
workspace::restart(&Default::default(), cx);
return;
}
}
auto_update::check(&Default::default(), cx);
})
.into_any(),
)
}
_ => None,
}
}

View File

@@ -8,7 +8,6 @@ mod incoming_call_notification;
mod notifications;
mod panel_settings;
pub mod project_shared_notification;
mod sharing_status_indicator;
use call::{report_call_event_for_room, ActiveCall, Room};
use gpui::{
@@ -42,7 +41,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
chat_panel::init(cx);
incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx);
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(toggle_mute);

View File

@@ -1,62 +0,0 @@
use crate::toggle_screen_sharing;
use call::ActiveCall;
use gpui::{
color::Color,
elements::{MouseEventHandler, Svg},
platform::{Appearance, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext,
};
use workspace::WorkspaceSettings;
pub fn init(cx: &mut AppContext) {
let active_call = ActiveCall::global(cx);
let mut status_indicator = None;
cx.observe(&active_call, move |call, cx| {
if let Some(room) = call.read(cx).room() {
if room.read(cx).is_screen_sharing() {
if status_indicator.is_none()
&& settings::get::<WorkspaceSettings>(cx).show_call_status_icon
{
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
}
} else if let Some(window) = status_indicator.take() {
window.update(cx, |cx| cx.remove_window());
}
} else if let Some(window) = status_indicator.take() {
window.update(cx, |cx| cx.remove_window());
}
})
.detach();
}
pub struct SharingStatusIndicator;
impl Entity for SharingStatusIndicator {
type Event = ();
}
impl View for SharingStatusIndicator {
fn ui_name() -> &'static str {
"SharingStatusIndicator"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let color = match cx.window_appearance() {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
Svg::new("icons/desktop.svg")
.with_color(color)
.constrained()
.with_width(18.)
.aligned()
})
.on_click(MouseButton::Left, |_, _, cx| {
toggle_screen_sharing(&Default::default(), cx)
})
.into_any()
}
}

View File

@@ -19,6 +19,7 @@ settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
zed-actions = { path = "../zed-actions" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -6,8 +6,12 @@ use gpui::{
};
use picker::{Picker, PickerDelegate, PickerEvent};
use std::cmp::{self, Reverse};
use util::ResultExt;
use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt,
};
use workspace::Workspace;
use zed_actions::OpenZedURL;
pub fn init(cx: &mut AppContext) {
cx.add_action(toggle_command_palette);
@@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await
};
let intercept_result = cx.read(|cx| {
let mut intercept_result = cx.read(|cx| {
if cx.has_global::<CommandPaletteInterceptor>() {
cx.global::<CommandPaletteInterceptor>()(&query, cx)
} else {
None
}
});
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
if parse_zed_link(&query).is_some() {
intercept_result = Some(CommandInterceptResult {
action: OpenZedURL { url: query.clone() }.boxed_clone(),
string: query.clone(),
positions: vec![],
})
}
}
if let Some(CommandInterceptResult {
action,
string,

View File

@@ -38,6 +38,10 @@ impl DiagnosticIndicator {
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(cx);
cx.notify();
}
_ => {}
})
.detach();

View File

@@ -57,7 +57,6 @@ log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand.workspace = true
schemars.workspace = true
serde.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub use_on_type_format: bool,
pub scrollbar: Scrollbar,
pub relative_line_numbers: bool,
@@ -33,6 +34,7 @@ pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,

View File

@@ -1333,7 +1333,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
&r#"one
two
three
@@ -1344,9 +1344,22 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"ˇone
&r#"one
two
three
four
five
ˇ
six"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
@@ -1366,32 +1379,6 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
four
five
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
sixˇ"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
ˇ
six"#
.unindent(),
);
@@ -5089,12 +5076,17 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
});
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
// Enable Prettier formatting for the same buffer, and ensure
// LSP is called instead of Prettier.
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
},
Some(tree_sitter_rust::language()),
@@ -5113,7 +5105,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
project.update(cx, |project, _| {
project.enable_test_prettier(&[]);
project.languages().add(Arc::new(language));
});
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
@@ -5231,7 +5226,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@@ -5430,9 +5427,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"});
cx.simulate_keystroke(" ");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("s");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.assert_editor_state(indoc! {"
one.second_completion
@@ -5494,12 +5491,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
});
cx.set_state("editorˇ");
cx.simulate_keystroke(".");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("c");
cx.simulate_keystroke("l");
cx.simulate_keystroke("o");
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions, cx);
});
@@ -7788,7 +7785,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
@@ -7801,7 +7798,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
@@ -7817,7 +7814,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]
@@ -7828,6 +7825,73 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
});
}
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
});
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let test_plugin = "test_plugin";
let _ = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let prettier_format_suffix = project.update(cx, |project, _| {
let suffix = project.enable_test_prettier(&[test_plugin]);
project.languages().add(Arc::new(language));
suffix
});
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let buffer_text = "one\ntwo\nthree\n";
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
});
format.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string() + prettier_format_suffix,
"Test prettier formatting was not applied to the original buffer text",
);
update_test_language_settings(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
});
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
});
format.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
"Autoformatting (via test prettier) was not applied to the original buffer text",
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View File

@@ -2428,7 +2428,7 @@ impl Element<Editor> for EditorElement {
}
let active = matches!(
editor.context_menu,
editor.context_menu.read().as_ref(),
Some(crate::ContextMenu::CodeActions(_))
);
@@ -2439,9 +2439,13 @@ impl Element<Editor> for EditorElement {
}
let visible_rows = start_row..start_row + line_layouts.len() as u32;
let mut hover = editor
.hover_state
.render(&snapshot, &style, visible_rows, cx);
let mut hover = editor.hover_state.render(
&snapshot,
&style,
visible_rows,
editor.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
);
let mode = editor.mode;
let mut fold_indicators = editor.render_fold_indicators(

View File

@@ -9,13 +9,15 @@ use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
};
use language::{
markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt;
use workspace::Workspace;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
this.hover_state.diagnostic_popover = None;
})?;
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
let blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
let hover_popover = InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
blocks: vec![inlay_hover.tooltip],
language: None,
rendered_content: None,
blocks,
parsed_content,
};
this.update(&mut cx, |this, cx| {
@@ -288,35 +293,38 @@ fn show_hover(
});
})?;
// Construct new hover popover from hover request
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
if hover_result.is_empty() {
return None;
let hover_result = hover_request.await.ok().flatten();
let hover_popover = match hover_result {
Some(hover_result) if !hover_result.is_empty() => {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = if let Some(range) = hover_result.range {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.end);
start..end
} else {
anchor..anchor
};
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
Some(InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Text(range),
blocks,
parsed_content,
})
}
// Create symbol range of anchors for highlighting and filtering
// of future requests.
let range = if let Some(range) = hover_result.range {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.end);
start..end
} else {
anchor..anchor
};
Some(InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Text(range),
blocks: hover_result.contents,
language: hover_result.language,
rendered_content: None,
})
});
_ => None,
};
this.update(&mut cx, |this, cx| {
if let Some(symbol_range) = hover_popover
@@ -345,44 +353,56 @@ fn show_hover(
editor.hover_state.info_task = Some(task);
}
fn render_blocks(
async fn parse_blocks(
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
) -> RichText {
let mut data = RichText {
text: Default::default(),
highlights: Default::default(),
region_ranges: Default::default(),
regions: Default::default(),
};
language: Option<Arc<Language>>,
) -> markdown::ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
for block in blocks {
match &block.kind {
HoverBlockKind::PlainText => {
new_paragraph(&mut data.text, &mut Vec::new());
data.text.push_str(&block.text);
markdown::new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text);
}
HoverBlockKind::Markdown => {
render_markdown_mut(&block.text, language_registry, language, &mut data)
markdown::parse_markdown_block(
&block.text,
language_registry,
language.clone(),
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await
}
HoverBlockKind::Code { language } => {
if let Some(language) = language_registry
.language_for_name(language)
.now_or_never()
.and_then(Result::ok)
{
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
} else {
data.text.push_str(&block.text);
text.push_str(&block.text);
}
}
}
}
data.text = data.text.trim().to_string();
data
ParsedMarkdown {
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
}
}
#[derive(Default)]
@@ -403,6 +423,7 @@ impl HoverState {
snapshot: &EditorSnapshot,
style: &EditorStyle,
visible_rows: Range<u32>,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// If there is a diagnostic, position the popovers based on that.
@@ -432,7 +453,7 @@ impl HoverState {
elements.push(diagnostic_popover.render(style, cx));
}
if let Some(info_popover) = self.info_popover.as_mut() {
elements.push(info_popover.render(style, cx));
elements.push(info_popover.render(style, workspace, cx));
}
Some((point, elements))
@@ -444,32 +465,23 @@ pub struct InfoPopover {
pub project: ModelHandle<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RichText>,
parsed_content: ParsedMarkdown,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
let rendered_content = self.rendered_content.get_or_insert_with(|| {
render_blocks(
&self.blocks,
self.project.read(cx).languages(),
self.language.as_ref(),
)
});
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
let code_span_background_color = style.document_highlight_read_background;
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
Flex::column()
.scrollable::<HoverBlock>(1, None, cx)
.with_child(rendered_content.element(
style.syntax.clone(),
style.text.clone(),
code_span_background_color,
.scrollable::<HoverBlock>(0, None, cx)
.with_child(crate::render_parsed_markdown::<HoverBlock>(
&self.parsed_content,
style,
workspace,
cx,
))
.contained()
@@ -572,7 +584,6 @@ mod tests {
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use rich_text::Highlight;
use smol::stream::StreamExt;
use unindent::Unindent;
use util::test::marked_text_ranges;
@@ -793,7 +804,7 @@ mod tests {
}],
);
let rendered = render_blocks(&blocks, &Default::default(), None);
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
assert_eq!(
rendered.text,
code_str.trim(),
@@ -900,7 +911,7 @@ mod tests {
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](the-url) three".to_string(),
text: "one [two](https://the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
@@ -921,7 +932,7 @@ mod tests {
- a
- b
* two
- [c](the-url)
- [c](https://the-url)
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
@@ -985,7 +996,7 @@ mod tests {
expected_styles,
} in &rows[0..]
{
let rendered = render_blocks(&blocks, &Default::default(), None);
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges
@@ -1001,11 +1012,8 @@ mod tests {
.highlights
.iter()
.filter_map(|(range, highlight)| {
let style = match highlight {
Highlight::Id(id) => id.style(&style.syntax)?,
Highlight::Highlight(style) => style.clone(),
};
Some((range.clone(), style))
let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight))
})
.collect();
@@ -1258,11 +1266,7 @@ mod tests {
"Popover range should match the new type label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for new type hint")
.text,
popover.parsed_content.text,
format!("A tooltip for `{new_type_label}`"),
"Rendered text should not anyhow alter backticks"
);
@@ -1316,11 +1320,7 @@ mod tests {
"Popover range should match the struct label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for struct hint")
.text,
popover.parsed_content.text,
format!("A tooltip for {struct_label}"),
"Rendered markdown element should remove backticks from text"
);

View File

@@ -234,7 +234,7 @@ pub fn start_of_paragraph(
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return map.max_point();
return DisplayPoint::zero();
}
let mut found_non_blank_line = false;
@@ -261,7 +261,7 @@ pub fn end_of_paragraph(
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return DisplayPoint::zero();
return map.max_point();
}
let mut found_non_blank_line = false;

View File

@@ -498,77 +498,91 @@ impl MultiBuffer {
}
}
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|edit| edit.range.start);
self.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
let mut edits = edits.into_iter().peekable();
let mut insertions = Vec::new();
let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into();
while let Some(BufferEdit {
mut range,
new_text,
mut is_insertion,
original_indent_column,
}) = edits.next()
{
drop(cursor);
drop(snapshot);
// Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
fn tail(
this: &mut MultiBuffer,
buffer_edits: HashMap<u64, Vec<BufferEdit>>,
autoindent_mode: Option<AutoindentMode>,
edited_excerpt_ids: Vec<ExcerptId>,
cx: &mut ModelContext<MultiBuffer>,
) {
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|edit| edit.range.start);
this.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
let mut edits = edits.into_iter().peekable();
let mut insertions = Vec::new();
let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into();
while let Some(BufferEdit {
range: next_range,
is_insertion: next_is_insertion,
..
}) = edits.peek()
mut range,
new_text,
mut is_insertion,
original_indent_column,
}) = edits.next()
{
if range.end >= next_range.start {
range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion;
edits.next();
} else {
break;
while let Some(BufferEdit {
range: next_range,
is_insertion: next_is_insertion,
..
}) = edits.peek()
{
if range.end >= next_range.start {
range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion;
edits.next();
} else {
break;
}
}
if is_insertion {
original_indent_columns.push(original_indent_column);
insertions.push((
buffer.anchor_before(range.start)
..buffer.anchor_before(range.end),
new_text.clone(),
));
} else if !range.is_empty() {
deletions.push((
buffer.anchor_before(range.start)
..buffer.anchor_before(range.end),
empty_str.clone(),
));
}
}
if is_insertion {
original_indent_columns.push(original_indent_column);
insertions.push((
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
new_text.clone(),
));
} else if !range.is_empty() {
deletions.push((
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
empty_str.clone(),
));
}
}
let deletion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns: Default::default(),
})
} else {
None
};
let insertion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
})
} else {
None
};
let deletion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns: Default::default(),
})
} else {
None
};
let insertion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
})
} else {
None
};
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
})
}
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
})
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
}
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
}
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {

View File

@@ -107,13 +107,23 @@ fn matching_history_item_paths(
) -> HashMap<Arc<Path>, PathMatch> {
let history_items_by_worktrees = history_items
.iter()
.map(|found_path| {
let path = &found_path.project.path;
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
path,
char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
path: &found_path.project.path,
// Only match history items names, otherwise their paths may match too many queries, producing false positives.
// E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
// it would be shown first always, despite the latter being a better match.
char_bag: CharBag::from_iter(
found_path
.project
.path
.file_name()?
.to_string_lossy()
.to_lowercase()
.chars(),
),
};
(found_path.project.worktree_id, candidate)
Some((found_path.project.worktree_id, candidate))
})
.fold(
HashMap::default(),
@@ -212,6 +222,10 @@ fn toggle_or_cycle_file_finder(
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
@@ -236,6 +250,16 @@ fn toggle_or_cycle_file_finder(
}
}
#[cfg(not(test))]
fn history_file_exists(abs_path: &PathBuf) -> bool {
abs_path.exists()
}
#[cfg(test)]
fn history_file_exists(abs_path: &PathBuf) -> bool {
!abs_path.ends_with("nonexistent.rs")
}
pub enum Event {
Selected(ProjectPath),
Dismissed,
@@ -505,12 +529,7 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local()
&& history_item
.absolute
.as_ref()
.filter(|abs_path| abs_path.exists())
.is_some())
|| (project.is_local() && history_item.absolute.is_some())
})
.cloned()
.map(|p| (p, None))
@@ -1803,6 +1822,202 @@ mod tests {
});
}
#[gpui::test]
async fn test_history_items_vs_very_good_external_match(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"collab_ui": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
"collab_ui.rs": "// Fourth Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"sec",
1,
"second.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
cx.dispatch_action(window.into(), Toggle);
let query = "collab_ui";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.delegate_mut().update_matches(query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
assert!(
delegate.matches.history.is_empty(),
"History items should not math query {query}, they should be matched by name only"
);
let search_entries = delegate
.matches
.search
.iter()
.map(|path_match| path_match.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/second.rs"),
],
"Despite all search results having the same directory name, the most matching one should be on top"
);
});
}
#[gpui::test]
async fn test_nonexistent_history_items_not_shown(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"nonexistent.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
// generate some history to select from
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"non",
1,
"nonexistent.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"thi",
1,
"third.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
open_close_queried_buffer(
"fir",
1,
"first.rs",
window.into(),
&workspace,
&deterministic,
cx,
)
.await;
cx.dispatch_action(window.into(), Toggle);
let query = "rs";
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.delegate_mut().update_matches(query.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let delegate = finder.delegate();
let history_entries = delegate
.matches
.history
.iter()
.map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
.collect::<Vec<_>>();
assert_eq!(
history_entries,
vec![
PathBuf::from("test/first.rs"),
PathBuf::from("test/third.rs"),
],
"Should have all opened files in the history, except the ones that do not exist on disk"
);
});
}
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,

View File

@@ -13,7 +13,6 @@ rope = { path = "../rope" }
text = { path = "../text" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
rpc = { path = "../rpc" }
anyhow.workspace = true
async-trait.workspace = true

View File

@@ -85,7 +85,7 @@ pub struct RemoveOptions {
pub ignore_if_not_exists: bool,
}
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug)]
pub struct Metadata {
pub inode: u64,
pub mtime: SystemTime,

View File

@@ -2,7 +2,6 @@ use anyhow::Result;
use collections::HashMap;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -23,6 +22,7 @@ pub struct Branch {
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
}
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -358,24 +358,6 @@ impl GitFileStatus {
}
}
}
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
git_status.and_then(|status| {
proto::GitStatus::from_i32(status).map(|status| match status {
proto::GitStatus::Added => GitFileStatus::Added,
proto::GitStatus::Modified => GitFileStatus::Modified,
proto::GitStatus::Conflict => GitFileStatus::Conflict,
})
})
}
pub fn to_proto(self) -> i32 {
match self {
GitFileStatus::Added => proto::GitStatus::Added as i32,
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
}
}
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]

View File

@@ -441,7 +441,7 @@ mod tests {
score,
worktree_id: 0,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},

View File

@@ -14,7 +14,7 @@ use crate::{
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub path: &'a Path,
pub char_bag: CharBag,
}
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: Arc::from(""),
distance_to_relative_ancestor: usize::MAX,
},
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path: Arc::from(candidate.path),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,

View File

@@ -71,7 +71,7 @@ pub struct Window {
pub(crate) hovered_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region_ids: Vec<MouseRegionId>,
pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
text_layout_cache: TextLayoutCache,
text_layout_cache: Arc<TextLayoutCache>,
refreshing: bool,
}
@@ -107,7 +107,7 @@ impl Window {
cursor_regions: Default::default(),
mouse_regions: Default::default(),
event_handlers: Default::default(),
text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())),
last_mouse_moved_event: None,
last_mouse_position: Vector2F::zero(),
pressed_buttons: Default::default(),
@@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> {
self.window.refreshing
}
pub fn text_layout_cache(&self) -> &TextLayoutCache {
pub fn text_layout_cache(&self) -> &Arc<TextLayoutCache> {
&self.window.text_layout_cache
}

View File

@@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
ViewContext,
};
use pathfinder_geometry::{
rect::RectF,
@@ -10,10 +11,10 @@ use pathfinder_geometry::{
};
use serde_json::json;
#[derive(Default)]
struct ScrollState {
scroll_to: Cell<Option<usize>>,
scroll_position: Cell<f32>,
type_tag: TypeTag,
}
pub struct Flex<V> {
@@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
where
Tag: 'static,
{
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
scroll_state.read(cx).scroll_to.set(scroll_to);
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
element_id,
Rc::new(ScrollState {
scroll_to: Cell::new(scroll_to),
scroll_position: Default::default(),
type_tag: TypeTag::new::<Tag>(),
}),
);
self.scroll_state = Some((scroll_state, cx.handle().id()));
self
}
@@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
if let Some((scroll_state, id)) = &self.scroll_state {
let scroll_state = scroll_state.read(cx).clone();
cx.scene().push_mouse_region(
crate::MouseRegion::new::<Self>(*id, 0, bounds)
.on_scroll({
let axis = self.axis;
move |e, _: &mut V, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
crate::MouseRegion::from_handlers(
scroll_state.type_tag,
*id,
0,
bounds,
Default::default(),
)
.on_scroll({
let axis = self.axis;
move |e, _: &mut V, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
let mut delta = match axis {
Axis::Horizontal => {
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
scroll_delta.y()
}
let mut delta = match axis {
Axis::Horizontal => {
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
scroll_delta.y()
}
Axis::Vertical => scroll_delta.y(),
};
if !e.delta.precise() {
delta *= 20.;
}
scroll_state
.scroll_position
.set(scroll_state.scroll_position.get() - delta);
cx.notify();
} else {
cx.propagate_event();
Axis::Vertical => scroll_delta.y(),
};
if !e.delta.precise() {
delta *= 20.;
}
scroll_state
.scroll_position
.set(scroll_state.scroll_position.get() - delta);
cx.notify();
} else {
cx.propagate_event();
}
})
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
}
})
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
)
}

View File

@@ -140,6 +140,10 @@ unsafe fn build_classes() {
sel!(application:openURLs:),
open_urls as extern "C" fn(&mut Object, Sel, id, id),
);
decl.add_method(
sel!(application:continueUserActivity:restorationHandler:),
continue_user_activity as extern "C" fn(&mut Object, Sel, id, id, id),
);
decl.register()
}
}
@@ -1009,6 +1013,26 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
}
}
extern "C" fn continue_user_activity(this: &mut Object, _: Sel, _: id, user_activity: id, _: id) {
let url = unsafe {
let url: id = msg_send!(user_activity, webpageURL);
if url == nil {
log::error!("got unexpected user activity");
None
} else {
Some(
CStr::from_ptr(url.absoluteString().UTF8String())
.to_string_lossy()
.to_string(),
)
}
};
let platform = unsafe { get_foreground_platform(this) };
if let Some(callback) = platform.0.borrow_mut().open_urls.as_mut() {
callback(url.into_iter().collect());
}
}
extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
unsafe {
let platform = get_foreground_platform(this);

View File

@@ -5,7 +5,7 @@ use crate::{
use anyhow::Result;
use gpui::{
geometry::{vector::Vector2F, Size},
text_layout::LineLayout,
text_layout::Line,
LayoutId,
};
use parking_lot::Mutex;
@@ -32,7 +32,7 @@ impl<V: 'static> Element<V> for Text {
_view: &mut V,
cx: &mut ViewContext<V>,
) -> Result<(LayoutId, Self::PaintState)> {
let fonts = cx.platform().fonts();
let layout_cache = cx.text_layout_cache().clone();
let text_style = cx.text_style();
let line_height = cx.font_cache().line_height(text_style.font_size);
let text = self.text.clone();
@@ -41,14 +41,14 @@ impl<V: 'static> Element<V> for Text {
let layout_id = cx.add_measured_layout_node(Default::default(), {
let paint_state = paint_state.clone();
move |_params| {
let line_layout = fonts.layout_line(
let line_layout = layout_cache.layout_str(
text.as_ref(),
text_style.font_size,
&[(text.len(), text_style.to_run())],
);
let size = Size {
width: line_layout.width,
width: line_layout.width(),
height: line_height,
};
@@ -85,13 +85,9 @@ impl<V: 'static> Element<V> for Text {
line_height = paint_state.line_height;
}
let text_style = cx.text_style();
let line =
gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
// TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
let visible_bounds = bounds;
line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
}
}
@@ -104,6 +100,6 @@ impl<V: 'static> IntoElement<V> for Text {
}
pub struct TextLayout {
line_layout: Arc<LineLayout>,
line_layout: Arc<Line>,
line_height: f32,
}

View File

@@ -22,7 +22,6 @@ test-support = [
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
@@ -46,6 +45,7 @@ lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
regex.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -1,11 +1,13 @@
pub use crate::{
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
markdown::ParsedMarkdown,
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
};
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings},
markdown::parse_markdown,
outline::OutlineItem,
syntax_map::{
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@@ -143,11 +145,51 @@ pub struct Diagnostic {
pub is_unnecessary: bool,
}
pub async fn prepare_completion_documentation(
documentation: &lsp::Documentation,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> Documentation {
match documentation {
lsp::Documentation::String(text) => {
if text.lines().count() <= 1 {
Documentation::SingleLine(text.clone())
} else {
Documentation::MultiLinePlainText(text.clone())
}
}
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
lsp::MarkupKind::PlainText => {
if value.lines().count() <= 1 {
Documentation::SingleLine(value.clone())
} else {
Documentation::MultiLinePlainText(value.clone())
}
}
lsp::MarkupKind::Markdown => {
let parsed = parse_markdown(value, language_registry, language).await;
Documentation::MultiLineMarkdown(parsed)
}
},
}
}
#[derive(Clone, Debug)]
pub enum Documentation {
Undocumented,
SingleLine(String),
MultiLinePlainText(String),
MultiLineMarkdown(ParsedMarkdown),
}
#[derive(Clone, Debug)]
pub struct Completion {
pub old_range: Range<Anchor>,
pub new_text: String,
pub label: CodeLabel,
pub documentation: Option<Documentation>,
pub server_id: LanguageServerId,
pub lsp_completion: lsp::CompletionItem,
}
@@ -1406,82 +1448,95 @@ impl Buffer {
return None;
}
self.start_transaction();
self.pending_autoindent.take();
let autoindent_request = autoindent_mode
.and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
// Non-generic part hoisted out to reduce LLVM IR size.
fn tail(
this: &mut Buffer,
edits: Vec<(Range<usize>, Arc<str>)>,
autoindent_mode: Option<AutoindentMode>,
cx: &mut ModelContext<Buffer>,
) -> Option<clock::Lamport> {
this.start_transaction();
this.pending_autoindent.take();
let autoindent_request = autoindent_mode
.and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode)));
let edit_operation = self.text.edit(edits.iter().cloned());
let edit_id = edit_operation.timestamp();
let edit_operation = this.text.edit(edits.iter().cloned());
let edit_id = edit_operation.timestamp();
if let Some((before_edit, mode)) = autoindent_request {
let mut delta = 0isize;
let entries = edits
.into_iter()
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), new_text)| {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta += new_text_length as isize - (range.end as isize - range.start as isize);
if let Some((before_edit, mode)) = autoindent_request {
let mut delta = 0isize;
let entries = edits
.into_iter()
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), new_text)| {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta +=
new_text_length as isize - (range.end as isize - range.start as isize);
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
if new_text.contains('\n')
&& old_start.column <= before_edit.indent_size_for_line(old_start.row).len
{
first_line_is_new = true;
}
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
// Avoid auto-indenting after the insertion.
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column =
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}));
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
if new_text.contains('\n')
&& old_start.column
<= before_edit.indent_size_for_line(old_start.row).len
{
first_line_is_new = true;
}
}
AutoindentRequestEntry {
first_line_is_new,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
}
})
.collect();
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
// Avoid auto-indenting after the insertion.
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column = Some(
original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}),
);
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
}
}
AutoindentRequestEntry {
first_line_is_new,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: this
.anchor_before(new_start + range_of_insertion_to_indent.start)
..this.anchor_after(new_start + range_of_insertion_to_indent.end),
}
})
.collect();
this.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
}
this.end_transaction(cx);
this.send_operation(Operation::Buffer(edit_operation), cx);
Some(edit_id)
}
self.end_transaction(cx);
self.send_operation(Operation::Buffer(edit_operation), cx);
Some(edit_id)
tail(self, edits, autoindent_mode, cx)
}
fn did_edit(

View File

@@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
// Insert the block at column zero. The entire block is indented
// so that the first line matches the previous line's indentation.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
[(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),

View File

@@ -2,6 +2,7 @@ mod buffer;
mod diagnostic_set;
mod highlight_map;
pub mod language_settings;
pub mod markdown;
mod outline;
pub mod proto;
mod syntax_map;
@@ -110,7 +111,6 @@ pub struct LanguageServerName(pub Arc<str>);
pub struct CachedLspAdapter {
pub name: LanguageServerName,
pub short_name: &'static str,
pub initialization_options: Option<Value>,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
@@ -121,7 +121,6 @@ impl CachedLspAdapter {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await;
let short_name = adapter.short_name();
let initialization_options = adapter.initialization_options().await;
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
let disk_based_diagnostics_progress_token =
adapter.disk_based_diagnostics_progress_token().await;
@@ -130,7 +129,6 @@ impl CachedLspAdapter {
Arc::new(CachedLspAdapter {
name,
short_name,
initialization_options,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
language_ids,
@@ -227,6 +225,10 @@ impl CachedLspAdapter {
) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
}
pub fn prettier_plugins(&self) -> &[&'static str] {
self.adapter.prettier_plugins()
}
}
pub trait LspAdapterDelegate: Send + Sync {
@@ -333,6 +335,10 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn language_ids(&self) -> HashMap<String, String> {
Default::default()
}
fn prettier_plugins(&self) -> &[&'static str] {
&[]
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -369,6 +375,8 @@ pub struct LanguageConfig {
pub overrides: HashMap<String, LanguageConfigOverride>,
#[serde(default)]
pub word_characters: HashSet<char>,
#[serde(default)]
pub prettier_parser_name: Option<String>,
}
#[derive(Debug, Default)]
@@ -442,6 +450,7 @@ impl Default for LanguageConfig {
overrides: Default::default(),
collapsed_placeholder: Default::default(),
word_characters: Default::default(),
prettier_parser_name: None,
}
}
}
@@ -467,6 +476,7 @@ pub struct FakeLspAdapter {
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>,
pub prettier_plugins: Vec<&'static str>,
}
#[derive(Clone, Debug, Default)]
@@ -1567,6 +1577,10 @@ impl Language {
override_id: None,
}
}
pub fn prettier_parser_name(&self) -> Option<&str> {
self.config.prettier_parser_name.as_deref()
}
}
impl LanguageScope {
@@ -1729,6 +1743,7 @@ impl Default for FakeLspAdapter {
disk_based_diagnostics_progress_token: None,
initialization_options: None,
disk_based_diagnostics_sources: Vec::new(),
prettier_plugins: Vec::new(),
}
}
}
@@ -1785,6 +1800,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
async fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone()
}
fn prettier_plugins(&self) -> &[&'static str] {
&self.prettier_plugins
}
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

View File

@@ -50,6 +50,7 @@ pub struct LanguageSettings {
pub remove_trailing_whitespace_on_save: bool,
pub ensure_final_newline_on_save: bool,
pub formatter: Formatter,
pub prettier: HashMap<String, serde_json::Value>,
pub enable_language_server: bool,
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
@@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
#[serde(default)]
pub formatter: Option<Formatter>,
#[serde(default)]
pub prettier: Option<HashMap<String, serde_json::Value>>,
#[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(default)]
pub show_copilot_suggestions: Option<bool>,
@@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting {
All,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Formatter {
#[default]
Auto,
LanguageServer,
Prettier,
External {
command: Arc<str>,
arguments: Arc<[String]>,
@@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.preferred_line_length,
);
merge(&mut settings.formatter, src.formatter.clone());
merge(&mut settings.prettier, src.prettier.clone());
merge(&mut settings.format_on_save, src.format_on_save.clone());
merge(
&mut settings.remove_trailing_whitespace_on_save,

View File

@@ -0,0 +1,301 @@
use std::sync::Arc;
use std::{ops::Range, path::PathBuf};
use crate::{HighlightId, Language, LanguageRegistry};
use gpui::fonts::{self, HighlightStyle, Weight};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
#[derive(Debug, Clone)]
pub struct ParsedMarkdown {
pub text: String,
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
pub region_ranges: Vec<Range<usize>>,
pub regions: Vec<ParsedRegion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownHighlight {
Style(MarkdownHighlightStyle),
Code(HighlightId),
}
impl MarkdownHighlight {
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
match self {
MarkdownHighlight::Style(style) => {
let mut highlight = HighlightStyle::default();
if style.italic {
highlight.italic = Some(true);
}
if style.underline {
highlight.underline = Some(fonts::Underline {
thickness: 1.0.into(),
..Default::default()
});
}
if style.weight != fonts::Weight::default() {
highlight.weight = Some(style.weight);
}
Some(highlight)
}
MarkdownHighlight::Code(id) => id.style(theme),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MarkdownHighlightStyle {
pub italic: bool,
pub underline: bool,
pub weight: Weight,
}
#[derive(Debug, Clone)]
pub struct ParsedRegion {
pub code: bool,
pub link: Option<Link>,
}
#[derive(Debug, Clone)]
pub enum Link {
Web { url: String },
Path { path: PathBuf },
}
impl Link {
fn identify(text: String) -> Option<Link> {
if text.starts_with("http") {
return Some(Link::Web { url: text });
}
let path = PathBuf::from(text);
if path.is_absolute() {
return Some(Link::Path { path });
}
None
}
}
pub async fn parse_markdown(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
parse_markdown_block(
markdown,
language_registry,
language,
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await;
ParsedMarkdown {
text,
highlights,
region_ranges,
regions,
}
}
pub async fn parse_markdown_block(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
region_ranges: &mut Vec<Range<usize>>,
regions: &mut Vec<ParsedRegion>,
) {
let mut bold_depth = 0;
let mut italic_depth = 0;
let mut link_url = None;
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(&markdown, Options::all()) {
let prev_len = text.len();
match event {
Event::Text(t) => {
if let Some(language) = &current_language {
highlight_code(text, highlights, t.as_ref(), language);
} else {
text.push_str(t.as_ref());
let mut style = MarkdownHighlightStyle::default();
if bold_depth > 0 {
style.weight = Weight::BOLD;
}
if italic_depth > 0 {
style.italic = true;
}
if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
region_ranges.push(prev_len..text.len());
regions.push(ParsedRegion {
code: false,
link: Some(link),
});
style.underline = true;
}
if style != MarkdownHighlightStyle::default() {
let mut new_highlight = true;
if let Some((last_range, MarkdownHighlight::Style(last_style))) =
highlights.last_mut()
{
if last_range.end == prev_len && last_style == &style {
last_range.end = text.len();
new_highlight = false;
}
}
if new_highlight {
let range = prev_len..text.len();
highlights.push((range, MarkdownHighlight::Style(style)));
}
}
}
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
let link = link_url.clone().and_then(|u| Link::identify(u));
if link.is_some() {
highlights.push((
prev_len..text.len(),
MarkdownHighlight::Style(MarkdownHighlightStyle {
underline: true,
..Default::default()
}),
));
}
regions.push(ParsedRegion { code: true, link });
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.await
.ok()
} else {
language.clone()
}
}
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));
}
Tag::Item => {
let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false;
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
for _ in 0..len - 1 {
text.push_str(" ");
}
if let Some(number) = list_number {
text.push_str(&format!("{}. ", number));
*number += 1;
*has_content = false;
} else {
text.push_str("- ");
}
}
}
_ => {}
},
Event::End(tag) => match tag {
Tag::Heading(_, _, _) => bold_depth -= 1,
Tag::CodeBlock(_) => current_language = None,
Tag::Emphasis => italic_depth -= 1,
Tag::Strong => bold_depth -= 1,
Tag::Link(_, _, _) => link_url = None,
Tag::List(_) => drop(list_stack.pop()),
_ => {}
},
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
_ => {}
}
}
}
pub fn highlight_code(
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
content: &str,
language: &Arc<Language>,
) {
let prev_len = text.len();
text.push_str(content);
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
let highlight = MarkdownHighlight::Code(highlight_id);
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
}
}
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
let mut is_subsequent_paragraph_of_list = false;
if let Some((_, has_content)) = list_stack.last_mut() {
if *has_content {
is_subsequent_paragraph_of_list = true;
} else {
*has_content = true;
return;
}
}
if !text.is_empty() {
if !text.ends_with('\n') {
text.push('\n');
}
text.push('\n');
}
for _ in 0..list_stack.len().saturating_sub(1) {
text.push_str(" ");
}
if is_subsequent_paragraph_of_list {
text.push_str(" ");
}
}

View File

@@ -482,6 +482,7 @@ pub async fn deserialize_completion(
lsp_completion.filter_text.as_deref(),
)
}),
documentation: None,
server_id: LanguageServerId(completion.server_id as usize),
lsp_completion,
})

View File

@@ -1,5 +1,5 @@
use collections::HashMap;
use editor::Editor;
use collections::{HashMap, VecDeque};
use editor::{Editor, MoveToEnd};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
@@ -11,7 +11,7 @@ use gpui::{
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
ViewContext, ViewHandle, WeakModelHandle,
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use language::{LanguageServerId, LanguageServerName};
use lsp::IoKind;
use project::{search::SearchQuery, Project};
use std::{borrow::Cow, sync::Arc};
@@ -22,8 +22,9 @@ use workspace::{
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
};
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
const SEND_LINE: &str = "// Send:";
const RECEIVE_LINE: &str = "// Receive:";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
pub struct LogStore {
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
@@ -36,24 +37,25 @@ struct ProjectState {
}
struct LanguageServerState {
log_buffer: ModelHandle<Buffer>,
log_messages: VecDeque<String>,
rpc_state: Option<LanguageServerRpcState>,
_io_logs_subscription: Option<lsp::Subscription>,
_lsp_logs_subscription: Option<lsp::Subscription>,
}
struct LanguageServerRpcState {
buffer: ModelHandle<Buffer>,
rpc_messages: VecDeque<String>,
last_message_kind: Option<MessageKind>,
}
pub struct LspLogView {
pub(crate) editor: ViewHandle<Editor>,
editor_subscription: Subscription,
log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
project: ModelHandle<Project>,
_log_store_subscription: Subscription,
_log_store_subscriptions: Vec<Subscription>,
}
pub struct LspLogToolbarItemView {
@@ -122,10 +124,9 @@ impl LogStore {
io_tx,
};
cx.spawn_weak(|this, mut cx| async move {
while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
message.push('\n');
this.on_io(project, server_id, io_kind, &message, cx);
});
}
@@ -168,15 +169,13 @@ impl LogStore {
project: &ModelHandle<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
) -> Option<&mut LanguageServerState> {
let project_state = self.projects.get_mut(&project.downgrade())?;
let server_state = project_state.servers.entry(id).or_insert_with(|| {
cx.notify();
LanguageServerState {
rpc_state: None,
log_buffer: cx
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
.clone(),
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
_io_logs_subscription: None,
_lsp_logs_subscription: None,
}
@@ -186,7 +185,7 @@ impl LogStore {
if let Some(server) = server.as_deref() {
if server.has_notification_handler::<lsp::notification::LogMessage>() {
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
return Some(server_state.log_buffer.clone());
return Some(server_state);
}
}
@@ -215,7 +214,7 @@ impl LogStore {
}
})
});
Some(server_state.log_buffer.clone())
Some(server_state)
}
fn add_language_server_log(
@@ -225,24 +224,26 @@ impl LogStore {
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let buffer = match self
let language_server_state = match self
.projects
.get_mut(&project.downgrade())?
.servers
.get(&id)
.map(|state| state.log_buffer.clone())
.get_mut(&id)
{
Some(existing_buffer) => existing_buffer,
Some(existing_state) => existing_state,
None => self.add_language_server(&project, id, cx)?,
};
buffer.update(cx, |buffer, cx| {
let len = buffer.len();
let has_newline = message.ends_with("\n");
buffer.edit([(len..len, message)], None, cx);
if !has_newline {
let len = buffer.len();
buffer.edit([(len..len, "\n")], None, cx);
}
let log_lines = &mut language_server_state.log_messages;
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front();
}
let message = message.trim();
log_lines.push_back(message.to_string());
cx.emit(Event::NewServerLogEntry {
id,
entry: message.to_string(),
is_rpc: false,
});
cx.notify();
Some(())
@@ -260,46 +261,32 @@ impl LogStore {
Some(())
}
pub fn log_buffer_for_server(
fn server_logs(
&self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
) -> Option<ModelHandle<Buffer>> {
) -> Option<&VecDeque<String>> {
let weak_project = project.downgrade();
let project_state = self.projects.get(&weak_project)?;
let server_state = project_state.servers.get(&server_id)?;
Some(server_state.log_buffer.clone())
Some(&server_state.log_messages)
}
fn enable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
) -> Option<&mut LanguageServerRpcState> {
let weak_project = project.downgrade();
let project_state = self.projects.get_mut(&weak_project)?;
let server_state = project_state.servers.get_mut(&server_id)?;
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
let language = project.read(cx).languages().language_for_name("JSON");
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
cx.spawn_weak({
let buffer = buffer.clone();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
});
}
})
.detach();
LanguageServerRpcState {
buffer,
let rpc_state = server_state
.rpc_state
.get_or_insert_with(|| LanguageServerRpcState {
rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
last_message_kind: None,
}
});
Some(rpc_state.buffer.clone())
});
Some(rpc_state)
}
pub fn disable_rpc_trace_for_language_server(
@@ -328,7 +315,7 @@ impl LogStore {
IoKind::StdIn => false,
IoKind::StdErr => {
let project = project.upgrade(cx)?;
let message = format!("stderr: {}\n", message.trim());
let message = format!("stderr: {}", message.trim());
self.add_language_server_log(&project, language_server_id, &message, cx);
return Some(());
}
@@ -341,24 +328,37 @@ impl LogStore {
.get_mut(&language_server_id)?
.rpc_state
.as_mut()?;
state.buffer.update(cx, |buffer, cx| {
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
};
let rpc_log_lines = &mut state.rpc_messages;
if state.last_message_kind != Some(kind) {
let line_before_message = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
if state.last_message_kind != Some(kind) {
let len = buffer.len();
let line = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
buffer.edit([(len..len, line)], None, cx);
state.last_message_kind = Some(kind);
}
let len = buffer.len();
buffer.edit([(len..len, message)], None, cx);
rpc_log_lines.push_back(line_before_message.to_string());
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: line_before_message.to_string(),
is_rpc: true,
});
}
while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
let message = message.trim();
rpc_log_lines.push_back(message.to_string());
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: message.to_string(),
is_rpc: true,
});
cx.notify();
Some(())
}
}
@@ -374,8 +374,7 @@ impl LspLogView {
.projects
.get(&project.downgrade())
.and_then(|project| project.servers.keys().copied().next());
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
(|| -> Option<()> {
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
if let Some(current_lsp) = this.current_server_id {
@@ -411,13 +410,31 @@ impl LspLogView {
cx.notify();
});
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
Event::NewServerLogEntry { id, entry, is_rpc } => {
if log_view.current_server_id == Some(*id) {
if (*is_rpc && log_view.is_showing_rpc_trace)
|| (!*is_rpc && !log_view.is_showing_rpc_trace)
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.handle_input(entry.trim(), cx);
editor.handle_input("\n", cx);
editor.set_read_only(true);
});
}
}
}
});
let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
let mut this = Self {
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
editor,
editor_subscription,
project,
log_store,
current_server_id: None,
is_showing_rpc_trace: false,
_log_store_subscription,
_log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
};
if let Some(server_id) = server_id {
this.show_logs_for_server(server_id, cx);
@@ -425,20 +442,19 @@ impl LspLogView {
this
}
fn editor_for_buffer(
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
fn editor_for_logs(
log_contents: String,
cx: &mut ViewContext<Self>,
) -> ViewHandle<Editor> {
) -> (ViewHandle<Editor>, Subscription) {
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
let mut editor = Editor::multi_line(None, cx);
editor.set_text(log_contents, cx);
editor.move_to_end(&MoveToEnd, cx);
editor.set_read_only(true);
editor.move_to_end(&Default::default(), cx);
editor
});
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach();
editor
let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
(editor, editor_subscription)
}
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
@@ -487,14 +503,17 @@ impl LspLogView {
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let buffer = self
let log_contents = self
.log_store
.read(cx)
.log_buffer_for_server(&self.project, server_id);
if let Some(buffer) = buffer {
.server_logs(&self.project, server_id)
.map(log_contents);
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = false;
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
self.editor = editor;
self.editor_subscription = editor_subscription;
cx.notify();
}
}
@@ -504,13 +523,37 @@ impl LspLogView {
server_id: LanguageServerId,
cx: &mut ViewContext<Self>,
) {
let buffer = self.log_store.update(cx, |log_set, cx| {
log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.enable_rpc_trace_for_language_server(&self.project, server_id)
.map(|state| log_contents(&state.rpc_messages))
});
if let Some(buffer) = buffer {
if let Some(rpc_log) = rpc_log {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = true;
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn_weak({
let buffer = cx.handle();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
});
}
})
.detach();
});
self.editor = editor;
self.editor_subscription = editor_subscription;
cx.notify();
}
}
@@ -523,7 +566,7 @@ impl LspLogView {
) {
self.log_store.update(cx, |log_store, cx| {
if enabled {
log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
} else {
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
}
@@ -535,6 +578,16 @@ impl LspLogView {
}
}
fn log_contents(lines: &VecDeque<String>) -> String {
let (a, b) = lines.as_slices();
let log_contents = a.join("\n");
if b.is_empty() {
log_contents
} else {
log_contents + "\n" + &b.join("\n")
}
}
impl View for LspLogView {
fn ui_name() -> &'static str {
"LspLogView"
@@ -685,6 +738,7 @@ impl View for LspLogToolbarItemView {
});
let server_selected = current_server.is_some();
enum LspLogScroll {}
enum Menu {}
let lsp_menu = Stack::new()
.with_child(Self::render_language_server_menu_header(
@@ -697,7 +751,7 @@ impl View for LspLogToolbarItemView {
Overlay::new(
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
Flex::column()
.scrollable::<Self>(0, None, cx)
.scrollable::<LspLogScroll>(0, None, cx)
.with_children(menu_rows.into_iter().map(|row| {
Self::render_language_server_menu_item(
row.server_id,
@@ -876,6 +930,7 @@ impl LspLogToolbarItemView {
) -> impl Element<Self> {
enum ActivateLog {}
enum ActivateRpcTrace {}
enum LanguageServerCheckbox {}
Flex::column()
.with_child({
@@ -921,7 +976,7 @@ impl LspLogToolbarItemView {
.with_height(theme.toolbar_dropdown_menu.row_height),
)
.with_child(
ui::checkbox_with_label::<Self, _, Self, _>(
ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
Empty::new(),
&theme.welcome.checkbox,
rpc_trace_enabled,
@@ -947,8 +1002,16 @@ impl LspLogToolbarItemView {
}
}
pub enum Event {
NewServerLogEntry {
id: LanguageServerId,
entry: String,
is_rpc: bool,
},
}
impl Entity for LogStore {
type Event = ();
type Event = Event;
}
impl Entity for LspLogView {

View File

@@ -91,9 +91,8 @@ impl TestServer {
let identity = claims.sub.unwrap().to_string();
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.ok_or_else(|| anyhow!("room {:?} does not exist", room_name))?;
let room = (*server_rooms).entry(room_name.to_string()).or_default();
if room.client_rooms.contains_key(&identity) {
Err(anyhow!(
"{:?} attempted to join room {:?} twice",

View File

@@ -466,7 +466,10 @@ impl LanguageServer {
completion_item: Some(CompletionItemCapability {
snippet_support: Some(true),
resolve_support: Some(CompletionItemCapabilityResolveSupport {
properties: vec!["additionalTextEdits".to_string()],
properties: vec![
"documentation".to_string(),
"additionalTextEdits".to_string(),
],
}),
..Default::default()
}),
@@ -748,6 +751,15 @@ impl LanguageServer {
)
}
// some child of string literal (be it "" or ``) which is the child of an attribute
// <Foo className="bar" />
// <Foo className={`bar`} />
// <Foo className={something + "bar"} />
// <Foo className={something + "bar"} />
// const classes = "awesome ";
// <Foo className={classes} />
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,

View File

@@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
}
}
pub struct FakeNodeRuntime;
pub struct FakeNodeRuntime(Option<PrettierSupport>);
struct PrettierSupport {
plugins: Vec<&'static str>,
}
impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime)
Arc::new(FakeNodeRuntime(None))
}
pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
}
}
#[async_trait::async_trait]
impl NodeRuntime for FakeNodeRuntime {
async fn binary_path(&self) -> Result<PathBuf> {
unreachable!()
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
if let Some(prettier_support) = &self.0 {
prettier_support.binary_path().await
} else {
unreachable!()
}
}
async fn run_npm_subcommand(
&self,
directory: Option<&Path>,
subcommand: &str,
args: &[&str],
) -> anyhow::Result<Output> {
if let Some(prettier_support) = &self.0 {
prettier_support
.run_npm_subcommand(directory, subcommand, args)
.await
} else {
unreachable!()
}
}
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
if let Some(prettier_support) = &self.0 {
prettier_support.npm_package_latest_version(name).await
} else {
unreachable!()
}
}
async fn npm_install_packages(
&self,
directory: &Path,
packages: &[(&str, &str)],
) -> anyhow::Result<()> {
if let Some(prettier_support) = &self.0 {
prettier_support
.npm_install_packages(directory, packages)
.await
} else {
unreachable!()
}
}
}
impl PrettierSupport {
const PACKAGE_VERSION: &str = "0.0.1";
fn new(plugins: &[&'static str]) -> Self {
Self {
plugins: plugins.to_vec(),
}
}
}
#[async_trait::async_trait]
impl NodeRuntime for PrettierSupport {
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
Ok(PathBuf::from("prettier_fake_node"))
}
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
unreachable!()
}
async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
unreachable!()
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
if name == "prettier" || self.plugins.contains(&name) {
Ok(Self::PACKAGE_VERSION.to_string())
} else {
panic!("Unexpected package name: {name}")
}
}
async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
unreachable!()
async fn npm_install_packages(
&self,
_: &Path,
packages: &[(&str, &str)],
) -> anyhow::Result<()> {
assert_eq!(
packages.len(),
self.plugins.len() + 1,
"Unexpected packages length to install: {:?}, expected `prettier` + {:?}",
packages,
self.plugins
);
for (name, version) in packages {
assert!(
name == &"prettier" || self.plugins.contains(name),
"Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
name,
packages,
Self::PACKAGE_VERSION,
self.plugins
);
assert_eq!(
version,
&Self::PACKAGE_VERSION,
"Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
version,
packages,
Self::PACKAGE_VERSION,
self.plugins
);
}
Ok(())
}
}

View File

@@ -0,0 +1,34 @@
[package]
name = "prettier"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/prettier.rs"
doctest = false
[features]
test-support = []
[dependencies]
client = { path = "../client" }
collections = { path = "../collections"}
language = { path = "../language" }
gpui = { path = "../gpui" }
fs = { path = "../fs" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
futures.workspace = true
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }

View File

@@ -0,0 +1,489 @@
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use gpui::{AsyncAppContext, ModelHandle};
use language::language_settings::language_settings;
use language::{Buffer, Diff};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use util::paths::DEFAULT_PRETTIER_DIR;
pub enum Prettier {
Real(RealPrettier),
#[cfg(any(test, feature = "test-support"))]
Test(TestPrettier),
}
pub struct RealPrettier {
worktree_id: Option<usize>,
default: bool,
prettier_dir: PathBuf,
server: Arc<LanguageServer>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct TestPrettier {
worktree_id: Option<usize>,
prettier_dir: PathBuf,
default: bool,
}
#[derive(Debug)]
pub struct LocateStart {
pub worktree_root_path: Arc<Path>,
pub starting_path: Arc<Path>,
}
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
const PRETTIER_PACKAGE_NAME: &str = "prettier";
const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
impl Prettier {
pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
".prettierrc",
".prettierrc.json",
".prettierrc.json5",
".prettierrc.yaml",
".prettierrc.yml",
".prettierrc.toml",
".prettierrc.js",
".prettierrc.cjs",
"package.json",
"prettier.config.js",
"prettier.config.cjs",
".editorconfig",
];
#[cfg(any(test, feature = "test-support"))]
pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
pub async fn locate(
starting_path: Option<LocateStart>,
fs: Arc<dyn Fs>,
) -> anyhow::Result<PathBuf> {
let paths_to_check = match starting_path.as_ref() {
Some(starting_path) => {
let worktree_root = starting_path
.worktree_root_path
.components()
.into_iter()
.take_while(|path_component| {
path_component.as_os_str().to_string_lossy() != "node_modules"
})
.collect::<PathBuf>();
if worktree_root != starting_path.worktree_root_path.as_ref() {
vec![worktree_root]
} else {
let (worktree_root_metadata, start_path_metadata) = if starting_path
.starting_path
.as_ref()
== Path::new("")
{
let worktree_root_data =
fs.metadata(&worktree_root).await.with_context(|| {
format!(
"FS metadata fetch for worktree root path {worktree_root:?}",
)
})?;
(worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}), None)
} else {
let full_starting_path = worktree_root.join(&starting_path.starting_path);
let (worktree_root_data, start_path_data) = futures::try_join!(
fs.metadata(&worktree_root),
fs.metadata(&full_starting_path),
)
.with_context(|| {
format!("FS metadata fetch for starting path {full_starting_path:?}",)
})?;
(
worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}),
start_path_data,
)
};
match start_path_metadata {
Some(start_path_metadata) => {
anyhow::ensure!(worktree_root_metadata.is_dir,
"For non-empty start path, worktree root {starting_path:?} should be a directory");
anyhow::ensure!(
!start_path_metadata.is_dir,
"For non-empty start path, it should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!start_path_metadata.is_symlink,
"For non-empty start path, it should not be a symlink {starting_path:?}"
);
let file_to_format = starting_path.starting_path.as_ref();
let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
let mut current_path = worktree_root;
for path_component in file_to_format.components().into_iter() {
current_path = current_path.join(path_component);
paths_to_check.push_front(current_path.clone());
if path_component.as_os_str().to_string_lossy() == "node_modules" {
break;
}
}
paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
Vec::from(paths_to_check)
}
None => {
anyhow::ensure!(
!worktree_root_metadata.is_dir,
"For empty start path, worktree root should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!worktree_root_metadata.is_symlink,
"For empty start path, worktree root should not be a symlink {starting_path:?}"
);
worktree_root
.parent()
.map(|path| vec![path.to_path_buf()])
.unwrap_or_default()
}
}
}
}
None => Vec::new(),
};
match find_closest_prettier_dir(paths_to_check, fs.as_ref())
.await
.with_context(|| format!("finding prettier starting with {starting_path:?}"))?
{
Some(prettier_dir) => Ok(prettier_dir),
None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn start(
worktree_id: Option<usize>,
_: LanguageServerId,
prettier_dir: PathBuf,
_: Arc<dyn NodeRuntime>,
_: AsyncAppContext,
) -> anyhow::Result<Self> {
Ok(
#[cfg(any(test, feature = "test-support"))]
Self::Test(TestPrettier {
worktree_id,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}),
)
}
#[cfg(not(any(test, feature = "test-support")))]
pub async fn start(
worktree_id: Option<usize>,
server_id: LanguageServerId,
prettier_dir: PathBuf,
node: Arc<dyn NodeRuntime>,
cx: AsyncAppContext,
) -> anyhow::Result<Self> {
use lsp::LanguageServerBinary;
let backgroud = cx.background();
anyhow::ensure!(
prettier_dir.is_dir(),
"Prettier dir {prettier_dir:?} is not a directory"
);
let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
anyhow::ensure!(
prettier_server.is_file(),
"no prettier server package found at {prettier_server:?}"
);
let node_path = backgroud
.spawn(async move { node.binary_path().await })
.await?;
let server = LanguageServer::new(
server_id,
LanguageServerBinary {
path: node_path,
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
},
Path::new("/"),
None,
cx,
)
.context("prettier server creation")?;
let server = backgroud
.spawn(server.initialize(None))
.await
.context("prettier server initialization")?;
Ok(Self::Real(RealPrettier {
worktree_id,
server,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}))
}
pub async fn format(
&self,
buffer: &ModelHandle<Buffer>,
buffer_path: Option<PathBuf>,
cx: &AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
Self::Real(local) => {
let params = buffer.read_with(cx, |buffer, cx| {
let buffer_language = buffer.language();
let parser_with_plugins = buffer_language.and_then(|l| {
let prettier_parser = l.prettier_parser_name()?;
let mut prettier_plugins = l
.lsp_adapters()
.iter()
.flat_map(|adapter| adapter.prettier_plugins())
.collect::<Vec<_>>();
prettier_plugins.dedup();
Some((prettier_parser, prettier_plugins))
});
let prettier_node_modules = self.prettier_dir().join("node_modules");
anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
let plugin_name_into_path = |plugin_name: &str| {
let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
for possible_plugin_path in [
prettier_plugin_dir.join("dist").join("index.mjs"),
prettier_plugin_dir.join("dist").join("index.js"),
prettier_plugin_dir.join("dist").join("plugin.js"),
prettier_plugin_dir.join("index.mjs"),
prettier_plugin_dir.join("index.js"),
prettier_plugin_dir.join("plugin.js"),
prettier_plugin_dir,
] {
if possible_plugin_path.is_file() {
return Some(possible_plugin_path);
}
}
None
};
let (parser, located_plugins) = match parser_with_plugins {
Some((parser, plugins)) => {
// Tailwind plugin requires being added last
// https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
let mut add_tailwind_back = false;
let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
add_tailwind_back = true;
false
} else {
true
}
}).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
if add_tailwind_back {
plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
}
(Some(parser.to_string()), plugins)
},
None => (None, Vec::new()),
};
let prettier_options = if self.is_default() {
let language_settings = language_settings(buffer_language, buffer.file(), cx);
let mut options = language_settings.prettier.clone();
if !options.contains_key("tabWidth") {
options.insert(
"tabWidth".to_string(),
serde_json::Value::Number(serde_json::Number::from(
language_settings.tab_size.get(),
)),
);
}
if !options.contains_key("printWidth") {
options.insert(
"printWidth".to_string(),
serde_json::Value::Number(serde_json::Number::from(
language_settings.preferred_line_length,
)),
);
}
Some(options)
} else {
None
};
let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
match located_plugin_path {
Some(path) => Some(path),
None => {
log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
None},
}
}).collect();
log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
anyhow::Ok(FormatParams {
text: buffer.text(),
options: FormatOptions {
parser,
plugins,
path: buffer_path,
prettier_options,
},
})
}).context("prettier params calculation")?;
let response = local
.server
.request::<Format>(params)
.await
.context("prettier format request")?;
let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
Ok(diff_task.await)
}
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(buffer
.read_with(cx, |buffer, cx| {
let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
buffer.diff(formatted_text, cx)
})
.await),
}
}
pub async fn clear_cache(&self) -> anyhow::Result<()> {
match self {
Self::Real(local) => local
.server
.request::<ClearCache>(())
.await
.context("prettier clear cache"),
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(()),
}
}
pub fn server(&self) -> Option<&Arc<LanguageServer>> {
match self {
Self::Real(local) => Some(&local.server),
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => None,
}
}
pub fn is_default(&self) -> bool {
match self {
Self::Real(local) => local.default,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => test_prettier.default,
}
}
pub fn prettier_dir(&self) -> &Path {
match self {
Self::Real(local) => &local.prettier_dir,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => &test_prettier.prettier_dir,
}
}
pub fn worktree_id(&self) -> Option<usize> {
match self {
Self::Real(local) => local.worktree_id,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => test_prettier.worktree_id,
}
}
}
async fn find_closest_prettier_dir(
paths_to_check: Vec<PathBuf>,
fs: &dyn Fs,
) -> anyhow::Result<Option<PathBuf>> {
for path in paths_to_check {
let possible_package_json = path.join("package.json");
if let Some(package_json_metadata) = fs
.metadata(&possible_package_json)
.await
.with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
{
if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
let package_json_contents = fs
.load(&possible_package_json)
.await
.with_context(|| format!("reading {possible_package_json:?} file contents"))?;
if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
&package_json_contents,
) {
if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
{
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
}
}
}
let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
if let Some(node_modules_location_metadata) = fs
.metadata(&possible_node_modules_location)
.await
.with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
{
if node_modules_location_metadata.is_dir {
return Ok(Some(path));
}
}
}
Ok(None)
}
enum Format {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatParams {
text: String,
options: FormatOptions,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatOptions {
plugins: Vec<PathBuf>,
parser: Option<String>,
#[serde(rename = "filepath")]
path: Option<PathBuf>,
prettier_options: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatResult {
text: String,
}
impl lsp::request::Request for Format {
type Params = FormatParams;
type Result = FormatResult;
const METHOD: &'static str = "prettier/format";
}
enum ClearCache {}
impl lsp::request::Request for ClearCache {
type Params = ();
type Result = ();
const METHOD: &'static str = "prettier/clear_cache";
}

View File

@@ -0,0 +1,217 @@
const { Buffer } = require('buffer');
const fs = require("fs");
const path = require("path");
const { once } = require('events');
const prettierContainerPath = process.argv[2];
if (prettierContainerPath == null || prettierContainerPath.length == 0) {
process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`);
process.exit(1);
}
fs.stat(prettierContainerPath, (err, stats) => {
if (err) {
process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
process.exit(1);
}
if (!stats.isDirectory()) {
process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`);
process.exit(1);
}
});
const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier');
class Prettier {
constructor(path, prettier, config) {
this.path = path;
this.prettier = prettier;
this.config = config;
}
}
(async () => {
let prettier;
let config;
try {
prettier = await loadPrettier(prettierPath);
config = await prettier.resolveConfig(prettierPath) || {};
} catch (e) {
process.stderr.write(`Failed to load prettier: ${e}\n`);
process.exit(1);
}
process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`);
process.stdin.resume();
handleBuffer(new Prettier(prettierPath, prettier, config));
})()
async function handleBuffer(prettier) {
for await (const messageText of readStdin()) {
let message;
try {
message = JSON.parse(messageText);
} catch (e) {
sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
continue;
}
// allow concurrent request handling by not `await`ing the message handling promise (async function)
handleMessage(message, prettier).catch(e => {
sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) });
});
}
}
const headerSeparator = "\r\n";
const contentLengthHeaderName = 'Content-Length';
async function* readStdin() {
let buffer = Buffer.alloc(0);
let streamEnded = false;
process.stdin.on('end', () => {
streamEnded = true;
});
process.stdin.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
});
async function handleStreamEnded(errorMessage) {
sendResponse(makeError(errorMessage));
buffer = Buffer.alloc(0);
messageLength = null;
await once(process.stdin, 'readable');
streamEnded = false;
}
try {
let headersLength = null;
let messageLength = null;
main_loop: while (true) {
if (messageLength === null) {
while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
if (streamEnded) {
await handleStreamEnded('Unexpected end of stream: headers not found');
continue main_loop;
} else if (buffer.length > contentLengthHeaderName.length * 10) {
await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`);
continue main_loop;
}
await once(process.stdin, 'readable');
}
const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii');
const contentLengthHeader = headers.split(headerSeparator)
.map(header => header.split(':'))
.filter(header => header[2] === undefined)
.filter(header => (header[1] || '').length > 0)
.find(header => (header[0] || '').trim() === contentLengthHeaderName);
const contentLength = (contentLengthHeader || [])[1];
if (contentLength === undefined) {
await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`);
continue main_loop;
}
headersLength = headers.length + headerSeparator.length * 2;
messageLength = parseInt(contentLength, 10);
}
while (buffer.length < (headersLength + messageLength)) {
if (streamEnded) {
await handleStreamEnded(
`Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`);
continue main_loop;
}
await once(process.stdin, 'readable');
}
const messageEnd = headersLength + messageLength;
const message = buffer.subarray(headersLength, messageEnd);
buffer = buffer.subarray(messageEnd);
headersLength = null;
messageLength = null;
yield message.toString('utf8');
}
} catch (e) {
sendResponse(makeError(`Error reading stdin: ${e}`));
} finally {
process.stdin.off('data', () => { });
}
}
async function handleMessage(message, prettier) {
const { method, id, params } = message;
if (method === undefined) {
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
}
if (id === undefined) {
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
}
if (method === 'prettier/format') {
if (params === undefined || params.text === undefined) {
throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`);
}
if (params.options === undefined) {
throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`);
}
let resolvedConfig = {};
if (params.options.filepath !== undefined) {
resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {};
}
const options = {
...(params.options.prettierOptions || prettier.config),
...resolvedConfig,
parser: params.options.parser,
plugins: params.options.plugins,
path: params.options.filepath
};
process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`);
const formattedText = await prettier.prettier.format(params.text, options);
sendResponse({ id, result: { text: formattedText } });
} else if (method === 'prettier/clear_cache') {
prettier.prettier.clearConfigCache();
prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {};
sendResponse({ id, result: null });
} else if (method === 'initialize') {
sendResponse({
id,
result: {
"capabilities": {}
}
});
} else {
throw new Error(`Unknown method: ${method}`);
}
}
function makeError(message) {
return {
error: {
"code": -32600, // invalid request code
message,
}
};
}
function sendResponse(response) {
const responsePayloadString = JSON.stringify({
jsonrpc: "2.0",
...response
});
const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`;
process.stdout.write(headers + responsePayloadString);
}
function loadPrettier(prettierPath) {
return new Promise((resolve, reject) => {
fs.access(prettierPath, fs.constants.F_OK, (err) => {
if (err) {
reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
} else {
try {
resolve(require(prettierPath));
} catch (err) {
reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`);
}
}
});
});
}

View File

@@ -15,6 +15,7 @@ test-support = [
"language/test-support",
"settings/test-support",
"text/test-support",
"prettier/test-support",
]
[dependencies]
@@ -31,6 +32,8 @@ git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime" }
prettier = { path = "../prettier" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
@@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
prettier = { path = "../prettier", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
git2.workspace = true

View File

@@ -10,7 +10,7 @@ use futures::future;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
language_settings::{language_settings, InlayHintKind},
point_from_lsp, point_to_lsp,
point_from_lsp, point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
@@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions {
async fn response_from_lsp(
self,
completions: Option<lsp::CompletionResponse>,
_: ModelHandle<Project>,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext,
@@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
}
}
} else {
Default::default()
Vec::new()
};
let completions = buffer.read_with(&cx, |buffer, _| {
let completions = buffer.read_with(&cx, |buffer, cx| {
let language_registry = project.read(cx).languages().clone();
let language = buffer.language().cloned();
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions {
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
if let Some(response_list) = &response_list {
if let Some(item_defaults) = &response_list.item_defaults {
if let Some(data) = &item_defaults.data {
lsp_completion.data = Some(data.clone());
}
}
}
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
// If the language server provides a range to overwrite, then
// check that the range is valid.
@@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions {
}
};
let language = language.clone();
LineEnding::normalize(&mut new_text);
let language_registry = language_registry.clone();
let language = language.clone();
Some(async move {
let mut label = None;
if let Some(language) = language {
if let Some(language) = language.as_ref() {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
}
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
Some(
prepare_completion_documentation(
lsp_docs,
&language_registry,
language.clone(),
)
.await,
)
} else {
None
};
Completion {
old_range,
new_text,
@@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
lsp_completion.filter_text.as_deref(),
)
}),
documentation,
server_id,
lsp_completion,
}

View File

@@ -20,7 +20,7 @@ use futures::{
mpsc::{self, UnboundedReceiver},
oneshot,
},
future::{try_join_all, Shared},
future::{self, try_join_all, Shared},
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
@@ -31,7 +31,9 @@ use gpui::{
};
use itertools::Itertools;
use language::{
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
language_settings::{
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -49,7 +51,9 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
};
use lsp_command::*;
use node_runtime::NodeRuntime;
use postage::watch;
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use search::SearchQuery;
@@ -75,10 +79,13 @@ use std::{
time::{Duration, Instant},
};
use terminals::Terminals;
use text::Anchor;
use text::{Anchor, LineEnding, Rope};
use util::{
debug_panic, defer, http::HttpClient, merge_json_value_into,
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
debug_panic, defer,
http::HttpClient,
merge_json_value_into,
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
post_inc, ResultExt, TryFutureExt as _,
};
pub use fs::*;
@@ -152,6 +159,11 @@ pub struct Project {
copilot_lsp_subscription: Option<gpui::Subscription>,
copilot_log_subscription: Option<lsp::Subscription>,
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
node: Option<Arc<dyn NodeRuntime>>,
prettier_instances: HashMap<
(Option<WorktreeId>, PathBuf),
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
>,
}
struct DelayedDebounced {
@@ -580,6 +592,7 @@ impl Project {
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);
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
client.add_model_request_handler(Self::handle_resolve_inlay_hint);
client.add_model_request_handler(Self::handle_refresh_inlay_hints);
client.add_model_request_handler(Self::handle_reload_buffers);
@@ -605,6 +618,7 @@ impl Project {
pub fn local(
client: Arc<Client>,
node: Arc<dyn NodeRuntime>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@@ -660,6 +674,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
node: Some(node),
prettier_instances: HashMap::default(),
}
})
}
@@ -757,6 +773,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
node: None,
prettier_instances: HashMap::default(),
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@@ -795,8 +813,16 @@ impl Project {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project =
cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx));
let project = cx.update(|cx| {
Project::local(
client,
node_runtime::FakeNodeRuntime::new(),
user_store,
Arc::new(languages),
fs,
cx,
)
});
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
@@ -810,19 +836,37 @@ impl Project {
project
}
/// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
/// Instead, if appends the suffix to every input, this suffix is returned by this method.
#[cfg(any(test, feature = "test-support"))]
pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
plugins,
));
Prettier::FORMAT_SUFFIX
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let mut language_servers_to_start = Vec::new();
let mut language_formatters_to_check = Vec::new();
for buffer in self.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) {
let buffer = buffer.read(cx);
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
let settings = language_settings(Some(language), Some(file), cx);
let buffer_file = File::from_dyn(buffer.file());
let buffer_language = buffer.language();
let settings = language_settings(buffer_language, buffer.file(), cx);
if let Some(language) = buffer_language {
if settings.enable_language_server {
if let Some(file) = File::from_dyn(Some(file)) {
if let Some(file) = buffer_file {
language_servers_to_start
.push((file.worktree.clone(), language.clone()));
.push((file.worktree.clone(), Arc::clone(language)));
}
}
language_formatters_to_check.push((
buffer_file.map(|f| f.worktree_id(cx)),
Arc::clone(language),
settings.clone(),
));
}
}
}
@@ -875,6 +919,11 @@ impl Project {
.detach();
}
for (worktree, language, settings) in language_formatters_to_check {
self.install_default_formatters(worktree, &language, &settings, cx)
.detach_and_log_err(cx);
}
// Start all the newly-enabled language servers.
for (worktree, language) in language_servers_to_start {
let worktree_path = worktree.read(cx).abs_path();
@@ -2623,7 +2672,26 @@ impl Project {
}
});
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
let buffer_file = buffer.read(cx).file().cloned();
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
let task_buffer = buffer.clone();
let prettier_installation_task =
self.install_default_formatters(worktree, &new_language, &settings, cx);
cx.spawn(|project, mut cx| async move {
prettier_installation_task.await?;
let _ = project
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(&task_buffer, cx)
})
.await;
anyhow::Ok(())
})
.detach_and_log_err(cx);
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);
@@ -2684,15 +2752,6 @@ impl Project {
let lsp = project_settings.lsp.get(&adapter.name.0);
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
let mut initialization_options = adapter.initialization_options.clone();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
}
(None, override_options) => initialization_options = override_options,
_ => {}
}
let server_id = pending_server.server_id;
let container_dir = pending_server.container_dir.clone();
let state = LanguageServerState::Starting({
@@ -2704,7 +2763,7 @@ impl Project {
cx.spawn_weak(|this, mut cx| async move {
let result = Self::setup_and_insert_language_server(
this,
initialization_options,
override_options,
pending_server,
adapter.clone(),
language.clone(),
@@ -2807,7 +2866,7 @@ impl Project {
async fn setup_and_insert_language_server(
this: WeakModelHandle<Self>,
initialization_options: Option<serde_json::Value>,
override_initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
@@ -2817,7 +2876,7 @@ impl Project {
) -> Result<Option<Arc<LanguageServer>>> {
let setup = Self::setup_pending_language_server(
this,
initialization_options,
override_initialization_options,
pending_server,
adapter.clone(),
server_id,
@@ -2849,7 +2908,7 @@ impl Project {
async fn setup_pending_language_server(
this: WeakModelHandle<Self>,
initialization_options: Option<serde_json::Value>,
override_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
server_id: LanguageServerId,
@@ -2867,8 +2926,8 @@ impl Project {
move |mut params, mut cx| {
let this = this;
let adapter = adapter.clone();
adapter.process_diagnostics(&mut params);
if let Some(this) = this.upgrade(&cx) {
adapter.process_diagnostics(&mut params);
this.update(&mut cx, |this, cx| {
this.update_diagnostics(
server_id,
@@ -2995,6 +3054,14 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options().await;
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
}
(None, override_options) => initialization_options = override_options,
_ => {}
}
let language_server = language_server.initialize(initialization_options).await?;
@@ -3949,7 +4016,7 @@ impl Project {
push_to_history: bool,
trigger: FormatTrigger,
cx: &mut ModelContext<Project>,
) -> Task<Result<ProjectTransaction>> {
) -> Task<anyhow::Result<ProjectTransaction>> {
if self.is_local() {
let mut buffers_with_paths_and_servers = buffers
.into_iter()
@@ -4027,6 +4094,7 @@ impl Project {
enum FormatOperation {
Lsp(Vec<(Range<Anchor>, String)>),
External(Diff),
Prettier(Diff),
}
// Apply language-specific formatting using either a language server
@@ -4062,8 +4130,8 @@ impl Project {
| (_, FormatOnSave::External { command, arguments }) => {
if let Some(buffer_abs_path) = buffer_abs_path {
format_operation = Self::format_via_external_command(
&buffer,
&buffer_abs_path,
buffer,
buffer_abs_path,
&command,
&arguments,
&mut cx,
@@ -4076,6 +4144,69 @@ impl Project {
.map(FormatOperation::External);
}
}
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
if let Some(prettier_task) = this
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(buffer, cx)
}).await {
match prettier_task.await
{
Ok(prettier) => {
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
});
format_operation = Some(FormatOperation::Prettier(
prettier
.format(buffer, buffer_path, &cx)
.await
.context("formatting via prettier")?,
));
}
Err(e) => anyhow::bail!(
"Failed to create prettier instance for buffer during autoformatting: {e:#}"
),
}
} else if let Some((language_server, buffer_abs_path)) =
language_server.as_ref().zip(buffer_abs_path.as_ref())
{
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&this,
&buffer,
buffer_abs_path,
&language_server,
tab_size,
&mut cx,
)
.await
.context("failed to format via language server")?,
));
}
}
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
if let Some(prettier_task) = this
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(buffer, cx)
}).await {
match prettier_task.await
{
Ok(prettier) => {
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
});
format_operation = Some(FormatOperation::Prettier(
prettier
.format(buffer, buffer_path, &cx)
.await
.context("formatting via prettier")?,
));
}
Err(e) => anyhow::bail!(
"Failed to create prettier instance for buffer during formatting: {e:#}"
),
}
}
}
};
buffer.update(&mut cx, |b, cx| {
@@ -4100,6 +4231,9 @@ impl Project {
FormatOperation::External(diff) => {
b.apply_diff(diff, cx);
}
FormatOperation::Prettier(diff) => {
b.apply_diff(diff, cx);
}
}
if let Some(transaction_id) = whitespace_transaction_id {
@@ -5873,6 +6007,7 @@ impl Project {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
this.update_prettier_settings(&worktree, changes, cx);
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
@@ -6252,6 +6387,69 @@ impl Project {
.detach();
}
fn update_prettier_settings(
&self,
worktree: &ModelHandle<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
cx: &mut ModelContext<'_, Project>,
) {
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
.iter()
.map(Path::new)
.collect::<HashSet<_>>();
let prettier_config_file_changed = changes
.iter()
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
.filter(|(path, _, _)| {
!path
.components()
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
})
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
let current_worktree_id = worktree.read(cx).id();
if let Some((config_path, _, _)) = prettier_config_file_changed {
log::info!(
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
);
let prettiers_to_reload = self
.prettier_instances
.iter()
.filter_map(|((worktree_id, prettier_path), prettier_task)| {
if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
cx.background()
.spawn(async move {
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
async move {
prettier_task.await?
.clear_cache()
.await
.with_context(|| {
format!(
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
)
})
.map_err(Arc::new)
}
}))
.await
{
if let Err(e) = task_result {
log::error!("Failed to clear cache for prettier: {e:#}");
}
}
})
.detach();
}
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -7155,6 +7353,40 @@ impl Project {
})
}
async fn handle_resolve_completion_documentation(
this: ModelHandle<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: ModelHandle<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,
@@ -8109,6 +8341,230 @@ impl Project {
Vec::new()
}
}
fn prettier_instance_for_buffer(
&mut self,
buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
let buffer = buffer.read(cx);
let buffer_file = buffer.file();
let Some(buffer_language) = buffer.language() else {
return Task::ready(None);
};
if buffer_language.prettier_parser_name().is_none() {
return Task::ready(None);
}
let buffer_file = File::from_dyn(buffer_file);
let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
let worktree_path = buffer_file
.as_ref()
.and_then(|file| Some(file.worktree.read(cx).abs_path()));
let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
let Some(node) = self.node.as_ref().map(Arc::clone) else {
return Task::ready(None);
};
cx.spawn(|this, mut cx| async move {
let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
let prettier_dir = match cx
.background()
.spawn(Prettier::locate(
worktree_path.zip(buffer_path).map(
|(worktree_root_path, starting_path)| LocateStart {
worktree_root_path,
starting_path,
},
),
fs,
))
.await
{
Ok(path) => path,
Err(e) => {
return Some(
Task::ready(Err(Arc::new(e.context(
"determining prettier path for worktree {worktree_path:?}",
))))
.shared(),
);
}
};
if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
project
.prettier_instances
.get(&(worktree_id, prettier_dir.clone()))
.cloned()
}) {
return Some(existing_prettier);
}
log::info!("Found prettier in {prettier_dir:?}, starting.");
let task_prettier_dir = prettier_dir.clone();
let weak_project = this.downgrade();
let new_server_id =
this.update(&mut cx, |this, _| this.languages.next_language_server_id());
let new_prettier_task = cx
.spawn(|mut cx| async move {
let prettier = Prettier::start(
worktree_id.map(|id| id.to_usize()),
new_server_id,
task_prettier_dir,
node,
cx.clone(),
)
.await
.context("prettier start")
.map_err(Arc::new)?;
log::info!("Started prettier in {:?}", prettier.prettier_dir());
if let Some((project, prettier_server)) =
weak_project.upgrade(&mut cx).zip(prettier.server())
{
project.update(&mut cx, |project, cx| {
let name = if prettier.is_default() {
LanguageServerName(Arc::from("prettier (default)"))
} else {
let prettier_dir = prettier.prettier_dir();
let worktree_path = prettier
.worktree_id()
.map(WorktreeId::from_usize)
.and_then(|id| project.worktree_for_id(id, cx))
.map(|worktree| worktree.read(cx).abs_path());
match worktree_path {
Some(worktree_path) => {
if worktree_path.as_ref() == prettier_dir {
LanguageServerName(Arc::from(format!(
"prettier ({})",
prettier_dir
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
)))
} else {
let dir_to_display = match prettier_dir
.strip_prefix(&worktree_path)
.ok()
{
Some(relative_path) => relative_path,
None => prettier_dir,
};
LanguageServerName(Arc::from(format!(
"prettier ({})",
dir_to_display.display(),
)))
}
}
None => LanguageServerName(Arc::from(format!(
"prettier ({})",
prettier_dir.display(),
))),
}
};
project
.supplementary_language_servers
.insert(new_server_id, (name, Arc::clone(prettier_server)));
cx.emit(Event::LanguageServerAdded(new_server_id));
});
}
Ok(Arc::new(prettier)).map_err(Arc::new)
})
.shared();
this.update(&mut cx, |project, _| {
project
.prettier_instances
.insert((worktree_id, prettier_dir), new_prettier_task.clone());
});
Some(new_prettier_task)
})
} else if self.remote_id().is_some() {
return Task::ready(None);
} else {
Task::ready(Some(
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
))
}
}
fn install_default_formatters(
&self,
worktree: Option<WorktreeId>,
new_language: &Language,
language_settings: &LanguageSettings,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
match &language_settings.formatter {
Formatter::Prettier { .. } | Formatter::Auto => {}
Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())),
};
let Some(node) = self.node.as_ref().cloned() else {
return Task::ready(Ok(()));
};
let mut prettier_plugins = None;
if new_language.prettier_parser_name().is_some() {
prettier_plugins
.get_or_insert_with(|| HashSet::default())
.extend(
new_language
.lsp_adapters()
.iter()
.flat_map(|adapter| adapter.prettier_plugins()),
)
}
let Some(prettier_plugins) = prettier_plugins else {
return Task::ready(Ok(()));
};
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
let already_running_prettier = self
.prettier_instances
.get(&(worktree, default_prettier_dir.to_path_buf()))
.cloned();
let fs = Arc::clone(&self.fs);
cx.background()
.spawn(async move {
let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE);
// method creates parent directory if it doesn't exist
fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await
.with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?;
let packages_to_versions = future::try_join_all(
prettier_plugins
.iter()
.chain(Some(&"prettier"))
.map(|package_name| async {
let returned_package_name = package_name.to_string();
let latest_version = node.npm_package_latest_version(package_name)
.await
.with_context(|| {
format!("fetching latest npm version for package {returned_package_name}")
})?;
anyhow::Ok((returned_package_name, latest_version))
}),
)
.await
.context("fetching latest npm versions")?;
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
(package.as_str(), version.as_str())
}).collect::<Vec<_>>();
node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
if !prettier_plugins.is_empty() {
if let Some(prettier) = already_running_prettier {
prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
}
}
anyhow::Ok(())
})
}
}
fn subscribe_for_copilot_events(

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